Flex KeyboardEvent Code

本文提供了一份详尽的键盘按键码对照表,包括常用字符键、功能键及特殊符号键等的键值映射。通过该表,开发者可以轻松识别各种键盘事件并进行程序响应。

keycode 8 = BackSpace BackSpace

keycode 9 = Tab Tab

keycode 12 = Clear

keycode 13 = Enter

keycode 16 = Shift_L

keycode 17 = Control_L

keycode 18 = Alt_L

keycode 19 = Pause

keycode 20 = Caps_Lock

keycode 27 = Escape Escape

keycode 32 = space space

keycode 33 = Prior

keycode 34 = Next

keycode 35 = End

keycode 36 = Home

keycode 37 = Left

keycode 38 = Up

keycode 39 = Right

keycode 40 = Down

keycode 41 = Select

keycode 42 = Print

keycode 43 = Execute

keycode 45 = Insert

keycode 46 = Delete

keycode 47 = Help

keycode 48 = 0 equal braceright

keycode 49 = 1 exclam onesuperior

keycode 50 = 2 quotedbl twosuperior

keycode 51 = 3 section threesuperior

keycode 52 = 4 dollar

keycode 53 = 5 percent

keycode 54 = 6 ampersand

keycode 55 = 7 slash braceleft

keycode 56 = 8 parenleft bracketleft

keycode 57 = 9 parenright bracketright

keycode 65 = a A

keycode 66 = b B

keycode 67 = c C

keycode 68 = d D

keycode 69 = e E EuroSign

keycode 70 = f F

keycode 71 = g G

keycode 72 = h H

keycode 73 = i I

keycode 74 = j J

keycode 75 = k K

keycode 76 = l L

keycode 77 = m M mu

keycode 78 = n N

keycode 79 = o O

keycode 80 = p P

keycode 81 = q Q at

keycode 82 = r R

keycode 83 = s S

keycode 84 = t T

keycode 85 = u U

keycode 86 = v V

keycode 87 = w W

keycode 88 = x X

keycode 89 = y Y

keycode 90 = z Z

keycode 96 = KP_0 KP_0

keycode 97 = KP_1 KP_1

keycode 98 = KP_2 KP_2

keycode 99 = KP_3 KP_3

keycode 100 = KP_4 KP_4

keycode 101 = KP_5 KP_5

keycode 102 = KP_6 KP_6

keycode 103 = KP_7 KP_7

keycode 104 = KP_8 KP_8

keycode 105 = KP_9 KP_9

keycode 106 = KP_Multiply KP_Multiply

keycode 107 = KP_Add KP_Add

keycode 108 = KP_Separator KP_Separator

keycode 109 = KP_Subtract KP_Subtract

keycode 110 = KP_Decimal KP_Decimal

keycode 111 = KP_Divide KP_Divide

keycode 112 = F1

keycode 113 = F2

keycode 114 = F3

keycode 115 = F4

keycod

Keycode 116 = F5

keycode 117 = F6

keycode 118 = F7

keycode 119 = F8

keycode 120 = F9

keycode 121 = F10

keycode 122 = F11

keycode 123 = F12

keycode 124 = F13

keycode 125 = F14

keycode 126 = F15

keycode 127 = F16

keycode 128 = F17

keycode 129 = F18

keycode 130 = F19

keycode 131 = F20

keycode 132 = F21

keycode 133 = F22

keycode 134 = F23

keycode 135 = F24

keycode 136 = Num_Lock

keycode 137 = Scroll_Lock

keycode 187 = acute grave

keycode 188 = comma semicolon

keycode 189 = minus underscore

keycode 190 = period colon

keycode 192 = numbersign apostrophe

keycode 210 = plusminus hyphen macron

keycode 211 =

keycode 212 = copyright registered

keycode 213 = guillemotleft guillemotright

keycode 214 = masculine ordfeminine

keycode 215 = ae AE

keycode 216 = cent yen

keycode 217 = questiondown exclamdown

keycode 218 = onequarter onehalf threequarters

keycode 220 = less greater bar

keycode 221 = plus asterisk asciitilde

keycode 227 = multiply division

keycode 228 = acircumflex Acircumflex

keycode 229 = ecircumflex Ecircumflex

keycode 230 = icircumflex Icircumflex

keycode 231 = ocircumflex Ocircumflex

keycode 232 = ucircumflex Ucircumflex

keycode 233 = ntilde Ntilde

keycode 234 = yacute Yacute

keycode 235 = oslash Ooblique

keycode 236 = aring Aring

keycode 237 = ccedilla Ccedilla

keycode 238 = thorn THORN

keycode 239 = eth ETH

keycode 240 = diaeresis cedilla currency

keycode 241 = agrave Agrave atilde Atilde

keycode 242 = egrave Egrave

keycode 243 = igrave Igrave

keycode 244 = ograve Ograve otilde Otilde

keycode 245 = ugrave Ugrave

keycode 246 = adiaeresis Adiaeresis

keycode 247 = ediaeresis Ediaeresis

keycode 248 = idiaeresis Idiaeresis

keycode 249 = odiaeresis Odiaeresis

keycode 250 = udiaeresis Udiaeresis

keycode 251 = ssharp question backslash

keycode 252 = asciicircum degree

keycode 253 = 3 sterling

keycode 254 = Mode_switch

使用 event 对象的 keyCode 属性判断输入的键值

eg : if(event.keyCode==13)alert( “ enter! ” );

键值对应表

A    0X65   U    0X85

B    0X66   V    0X86

C    0X67   W    0X87

D    0X68   X    0X88

E    0X69   Y    0X89

F    0X70   Z    0X90

G    0X71   0    0X48

H    0X72   1    0X49

I    0X73   2    0X50

J    0X74   3    0X51

K    0X75   4    0X52

L    0X76   5    0X53

M    0X77   6    0X54

N    0X78   7    0X55

O    0X79   8    0X56

P    0X80   9    0X57

Q    0X81   ESC    0X1B

R    0X82   CTRL   0X11

S    0X83   SHIFT   0X10

T    0X84   ENTER   0XD

本文转自:http://hi.baidu.com/chinakite/blog/item/37bd0fb38e4085aed8335a3e.html

<template> <div> <h3 text-center m-0 mb-20px>{{ t("login.login") }}</h3> <el-form ref="loginFormRef" :model="loginFormData" :rules="loginRules" size="large" :validate-on-rule-change="false"> <!-- 用户名 --> <el-form-item prop="username"> <el-input v-model.trim="loginFormData.username" :placeholder="t('login.username')"> <template #prefix> <el-icon> <User /> </el-icon> </template> </el-input> </el-form-item> <!-- 密码 --> <el-tooltip :visible="isCapsLock" :content="t('login.capsLock')" placement="right"> <el-form-item prop="password"> <el-input v-model.trim="loginFormData.password" :placeholder="t('login.password')" type="password" show-password @keyup="checkCapsLock" @keyup.enter="handleLoginSubmit"> <template #prefix> <el-icon> <Lock /> </el-icon> </template> </el-input> </el-form-item> </el-tooltip> <div class="flex-x-between w-full"> <el-checkbox v-model="loginFormData.rememberMe">{{ t("login.rememberMe") }}</el-checkbox> <el-link type="primary" underline="never" @click="toOtherForm('resetPwd')"> {{ t("login.forgetPassword") }} </el-link> </div> <!-- 登录按钮 --> <el-form-item> <el-button :loading="loading" type="primary" class="w-full" @click="handleLoginSubmit"> {{ t("login.login") }} </el-button> </el-form-item> </el-form> <div flex-center gap-10px> <el-text size="default">{{ t("login.noAccount") }}</el-text> <el-link type="primary" underline="never" @click="toOtherForm('register')"> {{ t("login.reg") }} </el-link> </div> <!-- 第三方登录 --> <div class="third-party-login"> <div class="divider-container"> <div class="divider-line"></div> <span class="divider-text">{{ t("login.otherLoginMethods") }}</span> <div class="divider-line"></div> </div> <div class="flex-center gap-x-5 w-full text-[var(--el-text-color-secondary)]"> <CommonWrapper> <div text-20px class="i-svg:wechat" /> </CommonWrapper> <CommonWrapper> <div text-20px cursor-pointer class="i-svg:qq" /> </CommonWrapper> <CommonWrapper> <div text-20px cursor-pointer class="i-svg:github" /> </CommonWrapper> <CommonWrapper> <div text-20px cursor-pointer class="i-svg:gitee" /> </CommonWrapper> </div> </div> </div> </template> <script setup lang="ts"> import type { FormInstance } from "element-plus"; import { User, Lock } from "@element-plus/icons-vue"; import { useI18n } from "vue-i18n"; import { useRoute, useRouter } from "vue-router"; import { ElMessage } from "element-plus"; // 引入核心依赖(移除 permissionStore 相关) import AuthAPI, { type LoginFormData } from "@/api/auth-api"; import { useUserStore } from "@/store"; // 仅保留用户Store import CommonWrapper from "@/components/CommonWrapper/index.vue"; import { AuthStorage } from "@/utils/auth"; import axios from "axios"; // 初始化工具实例(移除 permissionStore) const { t } = useI18n(); const userStore = useUserStore(); const route = useRoute(); const router = useRouter(); // 确保是全局正确的路由实例 // 表单核心状态(不变) const loginFormRef = ref<FormInstance>(); const loading = ref(false); const isCapsLock = ref(false); // 登录表单数据(不变) const loginFormData = ref<Omit<LoginFormData, "captchaKey" | "captchaCode">>({ username: "李四", password: "123", rememberMe: false }); // 表单验证规则(不变) const loginRules = computed(() => ({ username: [ { required: true, trigger: "blur", message: t("login.message.username.required") } ], password: [ { required: true, trigger: "blur", message: t("login.message.password.required") } ] })); /** * 登录提交(核心简化:移除动态路由逻辑,直接跳转) */ async function handleLoginSubmit() { try { const valid = await loginFormRef.value?.validate(); if (!valid) return; loading.value = true; // 1. 执行登录(存储 Token) await userStore.login(loginFormData.value); console.log("登录成功,表单数据:", loginFormData.value); // 2. 获取 Token 并校验(确保登录状态有效) const token = AuthStorage.getAccessToken(); if (!token) { throw new Error("Token 存储失败,请重试"); } console.log("当前 Token:", token); // 3. (可选)获取用户信息(仅存信息,不依赖其菜单) const infoRes = await axios({ url: "http://10.170.1.104:8978/admin/admin/info", method: "get", headers: { "Authorization": token } }); if (infoRes.data.code !== 200) { throw new Error(`获取用户信息失败:${infoRes.data.msg || "未知错误"}`); } console.log("用户信息:", infoRes.data.data); // 仅打印,不用于路由 // 4. 确定跳转路径(简化:redirect参数优先,无则跳首页) let redirectPath = "/"; // 兜底跳首页(静态路由,确保存在) // 如果有 redirect 参数(如之前访问 /dashboard 被拦截),则跳该路径 if (route.query.redirect && typeof route.query.redirect === "string") { const decodedRedirect = decodeURIComponent(route.query.redirect); // 仅允许跳静态路由中的路径(避免无效路径) const validStaticPaths = ["/", "/dashboard", "/profile"]; // 替换为你的静态路由路径 if (validStaticPaths.includes(decodedRedirect)) { redirectPath = decodedRedirect; } console.log("使用 redirect 参数跳转:", redirectPath); } console.log("最终跳转路径:", redirectPath); // // 5. 直接跳转(移除所有动态路由相关干扰) // await router.replace(redirectPath); // ElMessage.success("登录成功,已进入系统"); // 5. 直接跳转(添加详细日志) console.log("准备执行跳转,路由实例:", router); console.log("router.replace 方法是否存在:", typeof router.replace === "function"); console.log("跳转路径是否为字符串:", typeof redirectPath === "string"); try { console.log("开始执行 router.replace,路径:", redirectPath); const result = await router.replace(redirectPath); console.log("router.replace 执行成功,返回结果:", result); ElMessage.success("登录成功,已进入系统"); } catch (error) { console.error("router.replace 执行失败,错误详情:", error); // console.error("错误名称:", error.name); // console.error("错误消息:", error.message); // console.error("错误堆栈:", jumpError.stack); // ElMessage.error("跳转失败:" + jumpError.message); } } catch (error: any) { // 错误详情打印(便于排查) console.error("登录跳转失败详情:", { message: error.message, statusCode: error.response?.status, responseData: error.response?.data, requestUrl: error.response?.config?.url }); // 用户友好提示 const errorMsg = error.response?.data?.msg || error.message || "登录失败,请联系管理员"; ElMessage.error(errorMsg); } finally { loading.value = false; } } // 大写锁定检查(不变) function checkCapsLock(event: KeyboardEvent) { isCapsLock.value = event.getModifierState("CapsLock"); } // 切换表单(不变) const emit = defineEmits(["update:modelValue"]); function toOtherForm(type: "register" | "resetPwd") { emit("update:modelValue", type); } </script> <style lang="scss" scoped> .third-party-login { .divider-container { display: flex; align-items: center; margin: 40px 0; .divider-line { flex: 1; height: 1px; background: linear-gradient(to right, transparent, var(--el-border-color-light), transparent); } .divider-text { padding: 0 16px; font-size: 12px; color: var(--el-text-color-regular); white-space: nowrap; } } } </style> import { createApp } from "vue"; import App from "./App.vue"; import setupPlugins from "@/plugins"; // 导入router import router from "@/router"; // 暗黑主题样式 import "element-plus/theme-chalk/dark/css-vars.css"; import "vxe-table/lib/style.css"; // 暗黑模式自定义变量 import "@/styles/dark/css-vars.css"; import "@/styles/index.scss"; import "uno.css"; // 过渡动画 import "animate.css"; // 自动为某些默认事件(如 touchstart、wheel 等)添加 { passive: true },提升滚动性能并消除控制台的非被动事件监听警告 import "default-passive-events"; const app = createApp(App); // 注册插件 app.use(setupPlugins); app.use(router); app.mount("#app"); // src/router/index.ts import type { App } from "vue"; // ✅ 从 vue 导入 App 类型 import type { RouteRecordRaw } from "vue-router"; // ✅ RouteRecordRaw 仍从 vue-router 导入 import { createRouter, createWebHashHistory } from "vue-router"; // 1. 导入静态路由(根据你的项目实际路由调整,若没有单独文件可直接定义) const constantRoutes: RouteRecordRaw[] = [ { path: "/login", component: () => import("@/views/login/index.vue"), meta: { hidden: true }, }, { path: "/404", component: () => import("@/views/error/404.vue"), meta: { hidden: true }, }, { path: "/test-jump", name: "TestJump", component: () => import("../views/test/TestJump.vue"), // 新建空白组件 meta: { hidden: true }, }, { path: "/", component: () => import("@/layouts/index.vue"), // 你的布局组件路径 redirect: "/dashboard", children: [ { path: "dashboard", component: () => import("@/views/dashboard/index.vue"), // 你的首页路径 meta: { title: "首页", icon: "home" }, }, ], }, ]; // 2. 创建路由实例(这里用 hash 模式,避免后端配置,适合开发) const router = createRouter({ history: createWebHashHistory(), // 若用 history 模式,需改 createWebHistory() routes: constantRoutes, scrollBehavior: () => ({ left: 0, top: 0 }), // 跳转后滚动到顶部 }); // 3. 全局注册路由的方法(供 main.ts 使用) export function setupRouter(app: App<Element>) { app.use(router); } // 4. 关键:添加默认导出!!!(解决“不提供 default 导出”的核心) export default router; // 可选:保留命名导出(方便需要显式导入的场景,非必需) export { router, constantRoutes }; 为什么不能执行const result = await router.replace(redirectPath);,进行安全跳转
10-17
import React, { useState, useEffect, useCallback, useRef, useMemo } from 'react'; import { Card, Typography, Input, Select, Button, Form, Row, Col, Space, Table, message, Image, Popconfirm, Modal, Switch, Radio, Upload, Checkbox, Flex, Tabs } from 'antd'; import { SearchOutlined, ReloadOutlined, PlusOutlined, EditOutlined, DeleteOutlined, RedoOutlined, CloseOutlined, LeftOutlined, UploadOutlined } from '@ant-design/icons'; import { goodsListApi, goodsDelApi, goodsUpdownApi, goodsSaveApi } from '@/api/goods/index'; import { goodstypeApi } from '@/api/goodstype/index'; import { dataurl } from '@/api/url'; import ImageUploader from '@/components/ImageUploader'; import UEditor from '@/components/ueditor'; import { handleNumberInput, handleTextOnlyInput } from '@/components/business/index'; import styles from './index.module.css'; import type { ColumnsType, TablePaginationConfig } from 'antd/es/table'; import { useNavigate } from 'react-router-dom'; // ==================== 常量定义 ==================== const { Title } = Typography; const { Option } = Select; // 列表页常量 const PAGE_SIZE_OPTIONS = ['10', '20', '50']; const RECOMMEND_OPTIONS = [ { value: '1', label: '热销商品' }, { value: '2', label: '为你推荐' }, { value: '3', label: '猜你喜欢' } ] as const; const SPECIFICATION_OPTIONS = [ { value: '1', label: '单规格' }, { value: '0', label: '多规格' } ] as const; // 编辑页常量 const SPEC_TYPES = { SINGLE: 'single', MULTIPLE: 'multiple' } as const; const API_SPEC_TYPES = { SINGLE: '1', // 单规格 MULTIPLE: '0' // 多规格 } as const; const INITIAL_SPEC_VALUE = { name: '', price: '', originalPrice: '', stock: '', imageUrl: '' }; const SPEC_FIELD_NAMES = ['name', 'price', 'originalPrice', 'stock', 'imageUrl'] as const; // ==================== 类型定义 ==================== // 列表页类型 interface GoodsItem { id: number; title: string; price: string; stock: number; sold: number; status: number; sort: number; img: string; goods_type_id: number[]; synopsis?: string; type: number; // 1: 单规格, 0: 多规格 recommend: number[]; keyword?: string; unit?: string; imgs?: string; content?: string; params?: string; shipping_address?: string; courier_fee?: string; norm_arr?: NormArrItem[]; original_price?: string; } interface SearchFormValues { title?: string; keyword?: string; synopsis?: string; type?: string; recommend?: string; goods_type_id?: string; } interface ClassOption { value: number; label: string; } interface ApiResponse<T = any> { code: number; data: T; msg?: string; } interface GoodsListResponse { list: GoodsItem[]; total: number; type?: { value: number; label: string }[]; } // 编辑页类型 interface Category { id: number; title: string; status: number; createTime: string; } interface ApiRes<T> { code: number; msg: string; data: T; } interface CategoryRes { list: Category[]; } interface NormItem { name: string; value: string[]; } interface NormArrItem { name: string; price: number; original_price: number; stock: number; img: string; } interface StockData { specType: string; price?: string; original_price?: string; stock?: string; norm?: NormItem[]; norm_arr?: NormArrItem[]; collectedAt?: string; } interface FormData { category: string; name: string; desc: string; unit: string; enableVideo: boolean; video?: string; mainImg: string; carouselImgs: string[]; status: 'active' | 'inactive'; price?: string; originalPrice?: string; stock?: string; content: string; params: string; address: string; freight: string; sort: string; sold: string; recommend: string[]; keyword: string; specType?: 'single' | 'multiple'; specs?: Record<string, any>; } interface Spec { id: number; name: string; price: string; originalPrice: string; stock: string; imageUrl: string; } // ==================== 工具函数 ==================== // 列表页工具函数 const safeConvertToApiResponse = <T,>(response: any): ApiResponse<T> => { return response as ApiResponse<T>; }; const filterGoodsList = (list: GoodsItem[], searchValues: SearchFormValues): GoodsItem[] => { if (!searchValues || Object.keys(searchValues).length === 0) { return list; } return list.filter(item => { if (searchValues.title && !item.title?.toLowerCase().includes(searchValues.title.toLowerCase())) { return false; } if (searchValues.keyword) { const keyword = searchValues.keyword.toLowerCase(); const inTitle = item.title?.toLowerCase().includes(keyword); const inSynopsis = item.synopsis?.toLowerCase().includes(keyword); const inKeyword = item.keyword?.toLowerCase().includes(keyword); if (!inTitle && !inSynopsis && !inKeyword) { return false; } } if (searchValues.synopsis && !item.synopsis?.toLowerCase().includes(searchValues.synopsis.toLowerCase())) { return false; } if (searchValues.type && item.type.toString() !== searchValues.type) { return false; } if (searchValues.recommend && !item.recommend?.includes(parseInt(searchValues.recommend))) { return false; } if (searchValues.goods_type_id && !item.goods_type_id?.includes(parseInt(searchValues.goods_type_id))) { return false; } return true; }); }; // 编辑页工具函数 // 验证规则 const createSimpleRules = (fieldName: string) => [ { required: true, message: `${fieldName}不能为空` } ]; // 富文本编辑器工具函数 const processContent = (text: string, mode: 'save' | 'edit'): string => { if (!text) return ''; const div = document.createElement('div'); div.innerHTML = text; div.querySelectorAll('img').forEach(img => { const src = img.getAttribute('src'); if (!src) return; if (mode === 'save') { const index = src.indexOf('/upload'); if (index !== -1) { img.setAttribute('src', src.substring(index)); } } else { if (!src.startsWith('http')) { const newSrc = src.startsWith('/upload') ? src : `/upload${src.startsWith('/') ? '' : '/'}${src}`; img.setAttribute('src', `${dataurl}${newSrc}`); } } }); return div.innerHTML; }; // ==================== 自定义Hook ==================== // 商品列表搜索表单 Hook const useEditProductStorage = () => { const setEditProduct = useCallback((product: GoodsItem | null) => { if (product) { sessionStorage.setItem('Edittheproduct', JSON.stringify(product)); } else { sessionStorage.removeItem('Edittheproduct'); } }, []); const getEditProduct = useCallback((): GoodsItem | null => { try { const data = sessionStorage.getItem('Edittheproduct'); return data ? JSON.parse(data) : null; } catch { return null; } }, []); const clearEditProduct = useCallback(() => { sessionStorage.removeItem('Edittheproduct'); }, []); return { setEditProduct, getEditProduct, clearEditProduct }; }; // 分类数据 Hook const useCategories = () => { const [list, setList] = useState<Category[]>([]); const [loading, setLoading] = useState(false); const load = useCallback(async () => { try { setLoading(true); const res = await goodstypeApi().signIn({}); const apiRes = res as ApiRes<CategoryRes>; if (apiRes?.code === 1) { setList(apiRes.data?.list || []); } else { throw new Error(apiRes?.msg || '获取分类失败'); } } catch (error) { message.error(error instanceof Error ? error.message : '加载失败'); } finally { setLoading(false); } }, []); useEffect(() => { load(); }, [load]); return { list, loading, load }; }; // 规格管理 Hook // 规格管理 const useSpecs = (form: any) => { const [specType, setSpecType] = useState<'single' | 'multiple'>(SPEC_TYPES.SINGLE); const [specs, setSpecs] = useState<Spec[]>([{ id: 1, ...INITIAL_SPEC_VALUE }]); const [imageUploaderKeys, setImageUploaderKeys] = useState<Record<number, number>>({ 1: Date.now() }); const [isInitialized, setIsInitialized] = useState(false); // 生成新的规格ID const getNewId = useCallback(() => specs.length > 0 ? Math.max(...specs.map(item => item.id)) + 1 : 1, [specs]); // 添加规格 const addSpec = useCallback(() => { const newId = getNewId(); const newSpec = { id: newId, ...INITIAL_SPEC_VALUE }; setSpecs(prev => [...prev, newSpec]); setImageUploaderKeys(prev => ({ ...prev, [newId]: Date.now() })); }, [getNewId]); // 删除规格 const deleteSpec = useCallback((id: number) => { if (specs.length <= 1) { message.warning('至少保留一个规格'); return; } setSpecs(prev => prev.filter(item => item.id !== id)); // 清理表单数据 SPEC_FIELD_NAMES.forEach(field => { form.setFieldValue([`specs.${id}.${field}`], undefined); }); // 清理上传器key setImageUploaderKeys(prev => { const newKeys = { ...prev }; delete newKeys[id]; return newKeys; }); }, [specs.length, form]); // 重置规格 const resetSpec = useCallback((id: number) => { const resetValues = { ...INITIAL_SPEC_VALUE }; setSpecs(prev => prev.map(item => item.id === id ? { ...item, ...resetValues } : item )); SPEC_FIELD_NAMES.forEach(field => { form.setFieldValue([`specs.${id}.${field}`], resetValues[field as keyof typeof INITIAL_SPEC_VALUE]); }); setImageUploaderKeys(prev => ({ ...prev, [id]: Date.now() })); }, [form]); // 图片上传成功处理 const handleImageUploadSuccess = useCallback((id: number, url: string | string[]) => { const imageUrl = Array.isArray(url) ? url[0] : url; form.setFieldValue([`specs.${id}.imageUrl`], imageUrl); setSpecs(prev => prev.map(item => item.id === id ? { ...item, imageUrl } : item )); }, [form]); // 初始化多规格数据 const initializeSpecs = useCallback((norm_arr: NormArrItem[] = []) => { if (norm_arr.length === 0) { setIsInitialized(true); return; } // 根据后端数据创建规格项 const newSpecs: Spec[] = norm_arr.map((spec, index) => ({ id: index + 1, name: spec.name || '', price: spec.price?.toString() || '', originalPrice: spec.original_price?.toString() || '', stock: spec.stock?.toString() || '', imageUrl: spec.img || '' })); setSpecs(newSpecs); // 设置图片上传器keys const newKeys: Record<number, number> = {}; newSpecs.forEach(spec => { newKeys[spec.id] = Date.now() + spec.id; }); setImageUploaderKeys(newKeys); // 设置表单值 const specsFormData: Record<string, any> = {}; newSpecs.forEach(spec => { specsFormData[`specs.${spec.id}.name`] = spec.name; specsFormData[`specs.${spec.id}.price`] = spec.price; specsFormData[`specs.${spec.id}.originalPrice`] = spec.originalPrice; specsFormData[`specs.${spec.id}.stock`] = spec.stock; specsFormData[`specs.${spec.id}.imageUrl`] = spec.imageUrl; }); form.setFieldsValue(specsFormData); setIsInitialized(true); }, [form]); // 设置规格类型 const setSpecTypeWithInit = useCallback((type: 'single' | 'multiple') => { setSpecType(type); if (type === SPEC_TYPES.SINGLE) { setIsInitialized(true); } }, []); return { specType, setSpecType: setSpecTypeWithInit, specs, imageUploaderKeys, isInitialized, addSpec, deleteSpec, resetSpec, handleImageUploadSuccess, initializeSpecs }; }; // 库存数据处理 Hook const useStockData = (specType: 'single' | 'multiple') => { const getStockData = useCallback((values: any): StockData | null => { // 单规格处理 if (specType === SPEC_TYPES.SINGLE) { if (!values.price || !values.originalPrice || !values.stock) { message.error('请填写完整的单规格信息'); return null; } return { specType: API_SPEC_TYPES.SINGLE, price: values.price, original_price: values.originalPrice, stock: values.stock, collectedAt: new Date().toISOString() }; } // 多规格处理 const allSpecData = Object.keys(values) .filter(key => key.startsWith('specs.')) .reduce((acc: any, key) => { const [_, specId, field] = key.split('.'); if (!acc[specId]) acc[specId] = {}; acc[specId][field] = values[key]; return acc; }, {}); const validSpecData = Object.values(allSpecData).filter((data: any) => data.name?.trim() && data.price && data.originalPrice && data.stock && data.imageUrl ); if (validSpecData.length === 0) { message.error('请填写完整的规格信息'); return null; } // 生成多规格的norm和norm_arr const norm: NormItem[] = [{ name: "name", value: validSpecData.map((data: any) => data.name.trim()) }]; const norm_arr: NormArrItem[] = validSpecData.map((data: any) => ({ name: data.name.trim(), price: Number(data.price || '0'), original_price: Number(data.originalPrice || '0'), stock: Number(data.stock || '0'), img: data.imageUrl || '' })); return { specType: API_SPEC_TYPES.MULTIPLE, norm: norm, norm_arr: norm_arr, collectedAt: new Date().toISOString() }; }, [specType]); return { getStockData }; }; // 表单验证 Hook const useValidation = (form: any, specType: 'single' | 'multiple', specs: Spec[]) => { const validateStep = useCallback(async (step: number) => { try { let fieldsToValidate: string[] = []; switch (step) { case 1: fieldsToValidate = ['category', 'name', 'desc', 'unit', 'mainImg', 'carouselImgs', 'status']; break; case 2: if (specType === SPEC_TYPES.MULTIPLE) { specs.forEach((spec) => { SPEC_FIELD_NAMES.forEach(field => { fieldsToValidate.push(`specs.${spec.id}.${field}`); }); }); } else { fieldsToValidate = ['price', 'originalPrice', 'stock']; } break; case 3: fieldsToValidate = ['content']; break; case 4: fieldsToValidate = ['params']; break; case 5: fieldsToValidate = ['address', 'freight']; break; case 6: fieldsToValidate = ['sort', 'sold', 'recommend', 'keyword']; break; default: fieldsToValidate = []; } await form.validateFields(fieldsToValidate); return true; } catch (error: any) { message.error('请完善当前步骤的必填项'); return false; } }, [form, specType, specs]); return { validateStep }; }; // 初始化表单数据 Hook const useFormInitialization = (form: any, editor1: React.RefObject<any>, editor2: React.RefObject<any>, specHook: any, initialData: GoodsItem | null) => { const { initializeSpecs, setSpecType } = specHook; const { getEditProduct } = useEditProductStorage(); useEffect(() => { const initializeForm = async () => { try { // 新增商品时的初始化 // 新增商品时的初始化 if (!editProductData) { // 设置默认规格类型为单规格 setSpecType(SPEC_TYPES.SINGLE); return; } // 编辑商品的逻辑 const productData = JSON.parse(editProductData); const isSingleSpec = productData.type === 1; const specTypeValue = isSingleSpec ? SPEC_TYPES.SINGLE : SPEC_TYPES.MULTIPLE; setSpecType(specTypeValue); const initialValues: any = { category: productData.goods_type_id?.toString() || '', // 假设 goods_type_id 是一个数字 name: productData.title || '', desc: productData.synopsis || '', unit: productData.unit || '', mainImg: productData.img || '', carouselImgs: productData.imgs ? productData.imgs.split(',') : [], status: productData.status === 1 ? 'active' : 'inactive', content: processContent(productData.content || '', 'edit'), params: processContent(productData.params || '', 'edit'), address: productData.shipping_address || '', freight: productData.courier_fee || '', sort: productData.sort?.toString() || '0', sold: productData.sold?.toString() || '0', recommend: productData.recommend?.map((item: number) => item.toString()) || [], keyword: productData.keyword || '', specType: specTypeValue, }; if (isSingleSpec) { initialValues.price = productData.price || ''; initialValues.originalPrice = productData.original_price || ''; initialValues.stock = productData.stock?.toString() || ''; } else { // 确保 norm_arr 是一个数组 if (productData.norm_arr && Array.isArray(productData.norm_arr)) { initializeSpecs(productData.norm_arr); } else { initializeSpecs([]); // 传入空数组进行初始化 } } form.setFieldsValue(initialValues); // 设置富文本编辑器内容 const setEditorContent = () => { if (editor1.current && productData.content) { try { editor1.current.setContent(processContent(productData.content, 'edit')); } catch (error) { setTimeout(setEditorContent, 100); } } if (editor2.current && productData.params) { try { editor2.current.setContent(processContent(productData.params, 'edit')); } catch (error) { setTimeout(setEditorContent, 100); } } }; setTimeout(setEditorContent, 500); } catch (error) { console.error('初始化表单数据失败:', error); message.error('加载商品数据失败,使用默认表单'); } }; initializeForm(); }, [form, editor1, editor2, setSpecType, initializeSpecs]); }; // ==================== 组件定义 ==================== // 基础信息表单组件 // 基础信息表单组件 const BaseInfoForm: React.FC<{ form: any }> = ({ form }) => { const { list, loading } = useCategories(); const mainImgValue = Form.useWatch('mainImg', form); const carouselImgsValue = Form.useWatch('carouselImgs', form); const onMainImg = useCallback((url: string | string[]) => { const img = Array.isArray(url) ? url[0] : url; form.setFieldValue('mainImg', img); }, [form]); const onCarouselImgs = useCallback((urls: string | string[]) => { let newImages: string[] = []; if (Array.isArray(urls)) { newImages = urls; } else { newImages = [urls]; } // 获取当前已有的图片 const currentImages = form.getFieldValue('carouselImgs') || []; // 合并图片并去重 const allImages = [...currentImages, ...newImages]; const uniqueImages = Array.from(new Set(allImages)); form.setFieldValue('carouselImgs', uniqueImages); }, [form]); return ( <Form form={form} layout="horizontal" labelCol={{ span: 2 }} wrapperCol={{ span: 22 }} initialValues={{ status: 'active', enableVideo: false }} > <Form.Item label="商品分类" name="category" rules={createSimpleRules('商品分类')} > <Select placeholder="请选择商品分类" size="middle" allowClear className={styles.productNameInput} loading={loading} > {list.map(item => ( <Select.Option key={item.id} value={item.id.toString()}> {item.title} </Select.Option> ))} </Select> </Form.Item> <Form.Item label="商品名称" name="name" rules={createSimpleRules('商品名称')} > <Input placeholder="请输入商品名称" size="middle" className={styles.productNameInput} onChange={(e: React.ChangeEvent<HTMLInputElement>) => handleTextOnlyInput(form, 'name', e, { maxLength: 50 }) } onInput={(e: React.ChangeEvent<HTMLInputElement>) => handleTextOnlyInput(form, 'name', e, { maxLength: 50 }) } /> </Form.Item> <Form.Item label="商品简介" name="desc" rules={createSimpleRules('商品简介')} > <Input placeholder="请输入商品简介" size="middle" className={styles.productNameInput} onChange={(e: React.ChangeEvent<HTMLInputElement>) => handleTextOnlyInput(form, 'desc', e, { maxLength: 100 }) } onInput={(e: React.ChangeEvent<HTMLInputElement>) => handleTextOnlyInput(form, 'desc', e, { maxLength: 100 }) } /> </Form.Item> <Form.Item label="单位" name="unit" rules={createSimpleRules('单位')} > <Input placeholder="请输入单位" size="middle" maxLength={10} className={styles.productNameInput} onChange={(e: React.ChangeEvent<HTMLInputElement>) => handleTextOnlyInput(form, 'unit', e, { maxLength: 10 }) } onInput={(e: React.ChangeEvent<HTMLInputElement>) => handleTextOnlyInput(form, 'unit', e, { maxLength: 10 }) } /> </Form.Item> <Form.Item label="是否添加视频" name="enableVideo" valuePropName="checked"> <Switch /> </Form.Item> <Form.Item noStyle shouldUpdate={(prev, curr) => prev.enableVideo !== curr.enableVideo} > {({ getFieldValue }) => getFieldValue('enableVideo') ? ( <Form.Item label="商品视频" name="video" rules={createSimpleRules('商品视频')} > <div className={styles.videoUploadContainer}> <Upload maxCount={1} accept="video/mp4,video/avi"> <Button icon={<UploadOutlined />} size="middle" style={{ width: '100%' }}> 点击上传视频 </Button> </Upload> <Button danger icon={<DeleteOutlined />}>删除</Button> </div> </Form.Item> ) : null } </Form.Item> <Row className="imageUploadRow"> <Col span={6}> <Form.Item label="商品图片" name="mainImg" rules={createSimpleRules('商品图片')} labelCol={{ span: 8 }} style={{ margin: 0 }} wrapperCol={{ span: 12 }} className={styles.photoGraph} > <ImageUploader key={`main-img-${mainImgValue || 'empty'}`} maxCount={1} onUploadSuccess={onMainImg} imageUrl={mainImgValue} /> </Form.Item> </Col> <Col span={16}> <Form.Item label="轮播图" name="carouselImgs" className={styles.photoGraph} rules={[{ validator: (_, value) => !value || value.length < 2 ? Promise.reject(new Error('请上传至少两张轮播图')) : Promise.resolve() }]} > <ImageUploader key={`carousel-${carouselImgsValue?.length || 0}`} maxCount={4} onUploadSuccess={onCarouselImgs} imageUrl={carouselImgsValue} /> </Form.Item> </Col> </Row> <Form.Item label="状态" name="status" rules={createSimpleRules('商品状态')} style={{ marginTop: -20 }} > <Radio.Group size="large"> <Radio value="active">上架</Radio> <Radio value="inactive">下架</Radio> </Radio.Group> </Form.Item> </Form> ); }; // 规格配置组件 interface SpecConfigProps { form: any; specHook: any; } const SpecConfig: React.FC<SpecConfigProps> = ({ form, specHook }) => { const { specType, setSpecType, specs, imageUploaderKeys, isInitialized, addSpec, deleteSpec, resetSpec, handleImageUploadSuccess } = specHook; // 规格类型切换时的数据清理 useEffect(() => { if (!isInitialized) return; const currentValues = form.getFieldsValue(true); if (specType === SPEC_TYPES.SINGLE) { // 切换到单规格时清理多规格数据 const updatedValues = { ...currentValues }; specs.forEach(spec => { SPEC_FIELD_NAMES.forEach(field => { const fieldName = `specs.${spec.id}.${field}`; delete updatedValues[fieldName]; }); }); delete updatedValues.specs; form.setFieldsValue(updatedValues); } else { // 切换到多规格时清理单规格数据 const updatedValues = { ...currentValues }; delete updatedValues.price; delete updatedValues.originalPrice; delete updatedValues.stock; form.setFieldsValue(updatedValues); } }, [specType, form, specs, isInitialized]); const multiSpecs = useMemo(() => ( <> <Form.Item label="规格名称" required style={{ marginBottom: 16 }} > <div className={styles.specNameInputLayout}> <Button type="default" icon={<PlusOutlined />} onClick={addSpec}> 添加新规格 </Button> <div className={styles.specNameGrid}> {specs.map((spec) => ( <Form.Item key={`name-${spec.id}`} name={[`specs.${spec.id}.name`]} rules={createSimpleRules('规格名称')} required style={{ margin: 0 }} validateTrigger={['onChange', 'onBlur']} > <Input placeholder="请输入规格名称" suffix={specs.length > 1 && ( <DeleteOutlined onClick={() => deleteSpec(spec.id)} style={{ cursor: 'pointer', color: '#ff4d4f' }} /> )} /> </Form.Item> ))} </div> </div> </Form.Item> <Form.Item label="批量设置" required style={{ marginBottom: 16 }} > <table className={styles.batchTable}> <thead> <tr className={styles.tableHeader}> <th className={styles.tableHeaderCell}>图片</th> <th className={styles.tableHeaderCell}>售价</th> <th className={styles.tableHeaderCell}>原价</th> <th className={styles.tableHeaderCell}>库存</th> <th className={styles.tableHeaderCell}>操作</th> </tr> </thead> <tbody> {specs.map((spec) => ( <tr key={`row-${spec.id}`}> <td className={styles.pictureIncorrect}> <Form.Item name={[`specs.${spec.id}.imageUrl`]} rules={createSimpleRules('商品图片')} style={{ margin: 0 }} required > <div className={styles.specImageUploadContainer}> <ImageUploader key={`uploader-${spec.id}-${imageUploaderKeys[spec.id] || Date.now()}`} imageUrl={spec.imageUrl} onUploadSuccess={(url) => handleImageUploadSuccess(spec.id, url)} maxCount={1} /> </div> </Form.Item> </td> <td className={styles.tableBodyCell}> <Form.Item name={[`specs.${spec.id}.price`]} rules={createSimpleRules('售价')} style={{ margin: 0 }} required > <Input placeholder="请输入价格" onChange={(e: React.ChangeEvent<HTMLInputElement>) => handleNumberInput(form, `specs.${spec.id}.price`, e, { supportDecimal: true }) } /> </Form.Item> </td> <td className={styles.tableBodyCell}> <Form.Item name={[`specs.${spec.id}.originalPrice`]} rules={createSimpleRules('原价')} style={{ margin: 0 }} required > <Input placeholder="请输入原价" onChange={(e: React.ChangeEvent<HTMLInputElement>) => handleNumberInput(form, `specs.${spec.id}.originalPrice`, e, { supportDecimal: true }) } /> </Form.Item> </td> <td className={styles.tableBodyCell}> <Form.Item name={[`specs.${spec.id}.stock`]} rules={createSimpleRules('库存')} style={{ margin: 0 }} required > <Input placeholder="请输入库存" onChange={(e: React.ChangeEvent<HTMLInputElement>) => handleNumberInput(form, `specs.${spec.id}.stock`, e, { supportDecimal: false }) } /> </Form.Item> </td> <td className={styles.tableBodyCell}> <div className={styles.operationBtnLayout}> <Button className={styles.deleteButton} danger icon={<DeleteOutlined />} onClick={() => deleteSpec(spec.id)} disabled={specs.length <= 1} > 删除 </Button> <Button className={styles.resetButton} icon={<ReloadOutlined />} onClick={() => resetSpec(spec.id)} > 重置 </Button> </div> </td> </tr> ))} </tbody> </table> </Form.Item> </> ), [specs, imageUploaderKeys, addSpec, deleteSpec, resetSpec, handleImageUploadSuccess, form]); const singleSpec = useMemo(() => ( <> <Form.Item label="售价" name="price" rules={createSimpleRules('售价')} required style={{ marginBottom: 16 }} > <Input placeholder="请输入价格" className={styles.productNameInput} onChange={(e: React.ChangeEvent<HTMLInputElement>) => handleNumberInput(form, 'price', e, { supportDecimal: true }) } /> </Form.Item> <Form.Item label="原价" name="originalPrice" rules={createSimpleRules('原价')} required style={{ marginBottom: 16 }} > <Input placeholder="请输入原价" className={styles.productNameInput} onChange={(e: React.ChangeEvent<HTMLInputElement>) => handleNumberInput(form, 'originalPrice', e, { supportDecimal: true }) } /> </Form.Item> <Form.Item label="库存" name="stock" rules={createSimpleRules('库存')} required style={{ marginBottom: 16 }} > <Input placeholder="请输入库存" className={styles.productNameInput} onChange={(e: React.ChangeEvent<HTMLInputElement>) => handleNumberInput(form, 'stock', e, { supportDecimal: false }) } /> </Form.Item> </> ), [form]); if (!isInitialized) { return <div>加载规格数据中...</div>; } return ( <Form form={form} layout="horizontal" labelCol={{ span: 2 }} wrapperCol={{ span: 22 }} validateMessages={{ required: '${label}不能为空' }} > <Form.Item label="商品规格" required style={{ marginBottom: 16 }}> <div className={styles.specTypeLayout}> <Radio.Group value={specType} onChange={(e) => setSpecType(e.target.value)} > <Radio value={SPEC_TYPES.SINGLE}>单规格</Radio> <Radio value={SPEC_TYPES.MULTIPLE}>多规格</Radio> </Radio.Group> </div> </Form.Item> {specType === SPEC_TYPES.MULTIPLE ? multiSpecs : singleSpec} </Form> ); }; // 主组件 const GoodsManagement: React.FC = () => { // ==================== 状态管理 ==================== const [pageState, setPageState] = useState<'list' | 'edit'>('list'); const [currentGoods, setCurrentGoods] = useState<GoodsItem | null>(null); // 列表页状态 const [searchForm] = Form.useForm<SearchFormValues>(); const [allGoodsList, setAllGoodsList] = useState<GoodsItem[]>([]); const [filteredGoodsList, setFilteredGoodsList] = useState<GoodsItem[]>([]); const [displayGoodsList, setDisplayGoodsList] = useState<GoodsItem[]>([]); const [loading, setLoading] = useState(false); const [goodsTypeList, setGoodsTypeList] = useState<{ value: string; label: string }[]>([]); const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]); const [loadingStatus, setLoadingStatus] = useState<{ [key: number]: boolean }>({}); // 分页状态 const [pagination, setPagination] = useState<TablePaginationConfig>({ current: 1, pageSize: 10, total: 0, showSizeChanger: true, showQuickJumper: true, showTotal: (total) => `共 ${total} 条数据`, pageSizeOptions: PAGE_SIZE_OPTIONS, }); // 编辑页状态 const [editForm] = Form.useForm<FormData>(); const [activeTab, setActiveTab] = useState('1'); const editor1 = useRef<any>(null); const editor2 = useRef<any>(null); // 编辑页自定义Hook const specHook = useSpecs(editForm); const { specType, specs, isInitialized } = specHook; const { getStockData } = useStockData(specType); const { validateStep } = useValidation(editForm, specType, specs); // sessionStorage管理 const { setEditProduct, getEditProduct, clearEditProduct } = useEditProductStorage(); // 初始化编辑表单数据 useFormInitialization(editForm, editor1, editor2, specHook, currentGoods); const navigate = useNavigate(); // ==================== 列表页方法 ==================== // 获取商品列表数据 const getGoodsList = useCallback(async () => { setLoading(true); try { const response = await goodsListApi({ current: 1, pageSize: 1000 }); const apiResponse = safeConvertToApiResponse<GoodsListResponse>(response); if (apiResponse.code === 1) { const data = apiResponse.data || {}; const goodsData = data.list || []; setAllGoodsList(goodsData); setFilteredGoodsList(goodsData); setPagination(prev => ({ ...prev, total: goodsData.length, current: 1, })); if (data.type) { setGoodsTypeList(data.type.map(item => ({ value: item.value.toString(), label: item.label }))); } } else { message.error(apiResponse.msg || '获取数据失败'); } } catch (error) { console.error('获取数据失败:', error); message.error('网络错误,获取数据失败'); } finally { setLoading(false); } }, []); // 上下架状态切换函数 const handleStatusChange = useCallback(async (ids: number | number[], checked: boolean) => { try { const newStatus = checked ? 1 : 0; const isBatch = Array.isArray(ids); if (isBatch) { setLoading(true); } else { setLoadingStatus(prev => ({ ...prev, [ids]: true })); } const requestData = { ids: Array.isArray(ids) ? ids.join(',') : ids.toString(), status: newStatus.toString() }; const response = await goodsUpdownApi(requestData); const apiResponse = safeConvertToApiResponse(response); if (apiResponse.code === 1) { message.success(isBatch ? `批量${checked ? '上架' : '下架'}成功` : `商品已${checked ? '上架' : '下架'}`); } else { message.error(apiResponse.msg || `${isBatch ? '批量' : ''}${checked ? '上架' : '下架'}失败`); } // 无论成功失败都重新获取数据 getGoodsList(); } catch (error) { console.error('上下架操作失败:', error); message.error('操作失败,请重试'); // 出错也重新获取数据 getGoodsList(); } finally { if (Array.isArray(ids)) { setLoading(false); } else { setLoadingStatus(prev => ({ ...prev, [ids]: false })); } } }, [getGoodsList]); // 处理商品状态变化 const handleBatchStatusChange = useCallback((checked: boolean) => { if (selectedRowKeys.length === 0) { message.warning('请选择要操作的商品'); return; } const actionText = checked ? '上架' : '下架'; Modal.confirm({ title: `是否确认批量${actionText}?`, content: `即将${actionText} ${selectedRowKeys.length} 个商品`, okText: '确定', okType: 'danger', cancelText: '取消', centered: true, onOk: () => { const ids = selectedRowKeys.map(key => Number(key)); handleStatusChange(ids, checked); }, }); }, [selectedRowKeys, handleStatusChange]); // 删除商品功能 const handleDelete = useCallback(async (ids: React.Key[]) => { try { setAllGoodsList(prev => prev.filter(item => !ids.includes(item.id))); setFilteredGoodsList(prev => prev.filter(item => !ids.includes(item.id))); const response = await goodsDelApi({ ids }); const apiResponse = safeConvertToApiResponse(response); if (apiResponse.code === 1) { message.success(ids.length === 1 ? '删除成功' : '批量删除成功'); setSelectedRowKeys(prev => prev.filter(key => !ids.includes(key))); // 清除已选中的行 getGoodsList(); // 重新获取数据 } else { message.error('删除失败:' + (apiResponse.msg || '未知错误')); getGoodsList(); } } catch (error) { console.error('删除商品失败:', error); message.error('删除失败'); getGoodsList(); } }, [getGoodsList]); // 批量删除功能 const handleBatchDelete = useCallback(() => { if (selectedRowKeys.length === 0) { message.warning('请选择要删除的商品'); return; } Modal.confirm({ title: '是否确认批量删除?', content: `即将删除 ${selectedRowKeys.length} 条数据`, okText: '确定', okType: 'danger', cancelText: '取消', centered: true, onOk: () => handleDelete(selectedRowKeys) }); }, [selectedRowKeys, handleDelete]); // 处理本地搜索 const handleLocalSearch = useCallback((searchValues: SearchFormValues) => { const filteredList = filterGoodsList(allGoodsList, searchValues); setFilteredGoodsList(filteredList); const newPagination = { ...pagination, current: 1, total: filteredList.length }; setPagination(newPagination); setDisplayGoodsList(filteredList.slice(0, newPagination.pageSize || 10)); }, [allGoodsList, pagination]); // 处理分页变化 const handlePaginationChange = useCallback((current: number, pageSize: number) => { const startIndex = (current - 1) * pageSize; const endIndex = startIndex + pageSize; setDisplayGoodsList(filteredGoodsList.slice(startIndex, endIndex)); setPagination(prev => ({ ...prev, current, pageSize, total: filteredGoodsList.length })); }, [filteredGoodsList]); // 处理表格的分页、排序和筛选变化 const handleTableChange = useCallback((newPagination: TablePaginationConfig) => { const current = newPagination.current || 1; const pageSize = newPagination.pageSize || 10; handlePaginationChange(current, pageSize); setSelectedRowKeys([]); }, [handlePaginationChange]); // 搜索功能 const handleSearch = useCallback(() => { searchForm.validateFields().then(values => { handleLocalSearch(values); }); }, [searchForm, handleLocalSearch]); // 重置功能 const handleReset = useCallback(() => { searchForm.resetFields(); setFilteredGoodsList(allGoodsList); handlePaginationChange(1, pagination.pageSize || 10); setSelectedRowKeys([]); }, [searchForm, allGoodsList, pagination.pageSize, handlePaginationChange]); // 刷新功能 const handleRefresh = useCallback(() => { setSelectedRowKeys([]); getGoodsList(); }, [getGoodsList]); const handleInputKeyPress = useCallback((e: React.KeyboardEvent<HTMLInputElement>) => { if (e.key === 'Enter') { handleSearch(); } }, [handleSearch]); // 编辑商品 const handleEdit = useCallback((goods: GoodsItem) => { try { // 保存到sessionStorage setEditProduct(goods); setCurrentGoods(goods); setPageState('edit'); setActiveTab('1'); } catch (error) { console.error('准备编辑商品数据失败:', error); message.error('准备编辑商品失败'); } }, [setEditProduct]); // 新增商品 const handleAdd = useCallback(() => { // 清除编辑状态 clearEditProduct(); setCurrentGoods(null); setPageState('edit'); setActiveTab('1'); }, [clearEditProduct]); // ==================== 编辑页方法 ==================== // 同步富文本编辑器内容到表单 const syncEditorContent = useCallback(() => { if (editor1.current) { editForm.setFieldValue('content', editor1.current.getContent()); } if (editor2.current) { editForm.setFieldValue('params', editor2.current.getContent()); } }, [editForm]); // 切换标签页 const handleTabChange = useCallback(async (key: string) => { const currentStep = parseInt(activeTab, 10); const newStep = parseInt(key, 10); if (newStep > currentStep) { const isValid = await validateStep(currentStep); if (!isValid) return; } setActiveTab(key); }, [activeTab, validateStep]); // 切换步骤(上一步/下一步) const handleStepChange = useCallback(async (direction: 'prev' | 'next') => { const currentStep = parseInt(activeTab, 10); if (direction === 'next') { const isValid = await validateStep(currentStep); if (!isValid) return; } const newStep = direction === 'prev' ? Math.max(1, currentStep - 1) : Math.min(6, currentStep + 1); setActiveTab(newStep.toString()); }, [activeTab, validateStep]); // 保存商品 - 修复后的版本 const handleSave = useCallback(async () => { try { syncEditorContent(); // 验证所有步骤 for (let step = 1; step <= 6; step++) { const isValid = await validateStep(step); if (!isValid) { setActiveTab(step.toString()); return; } } const values = form.getFieldsValue(true); const stockData = getStockData(values); if (!stockData) return; const content = processContent(values.content || '', 'save'); const params = processContent(values.params || '', 'save'); // 构建提交数据 const submitData: any = { type: stockData.specType === API_SPEC_TYPES.SINGLE ? 1 : 0, goods_type_id: values.category ? Number(values.category) : '', title: values.name || "", synopsis: values.desc || "", unit: values.unit || "", img: values.mainImg || "", imgs: values.carouselImgs?.join(',') || "", status: values.status === 'active' ? 1 : 0, content: content, params: params, shipping_address: values.address || "", courier_fee: values.freight || "0.00", sort: Number(values.sort) || 0, sold: Number(values.sold) || 0, keyword: values.keyword || "", recommend: values.recommend?.join(',') || "", // 单规格数据 ...(stockData.specType === API_SPEC_TYPES.SINGLE ? { price: stockData.price || "0.00", original_price: stockData.original_price || "0.00", stock: Number(stockData.stock) || 0 } : { // 多规格数据 - 修复关键问题 norm: JSON.stringify(stockData.norm || []), norm_arr: JSON.stringify(stockData.norm_arr || []) }) }; // **核心改动:编辑时添加商品ID** const editProductData = sessionStorage.getItem('Edittheproduct'); if (editProductData) { const productData = JSON.parse(editProductData); if (productData.id !== undefined && productData.id !== null) { submitData.id = productData.id; // 将商品ID添加到提交数据中 } } console.log('提交数据:', submitData); // 调用保存接口 const response = await goodsSaveApi(submitData); if (response && response.code === 1) { message.success('商品保存成功!'); // 清除编辑状态 sessionStorage.removeItem('Edittheproduct'); // 延迟1.5秒后返回上一页 setTimeout(() => { window.history.back(); }, 1500); } else { const errorMsg = response?.msg || '商品保存失败,请稍后重试。'; message.error(errorMsg); } } catch (error: any) { console.error('保存失败详情:', error); message.error('保存失败,请检查网络或联系管理员。'); } }, [form, syncEditorContent, validateStep, getStockData]); // 返回列表页 const handleCancel = useCallback(() => { // 清除编辑状态 clearEditProduct(); setPageState('list'); setCurrentGoods(null); }, [clearEditProduct]); // 渲染操作按钮 const renderActionButtons = useCallback((tabKey: string) => { const isFirstStep = tabKey === '1'; const isLastStep = tabKey === '6'; return ( <Flex gap="small" style={{ marginTop: 24, padding: '0 20px' }}> {!isFirstStep && ( <Button onClick={() => handleStepChange('prev')}> 上一步 </Button> )} {!isLastStep ? ( <Button type="primary" onClick={() => handleStepChange('next')}> 下一步 </Button> ) : ( <Button type="primary" onClick={handleSave}> 保存 </Button> )} </Flex> ); }, [handleStepChange, handleSave]); // ==================== 渲染 ==================== // 列表页列定义 const columns: ColumnsType<GoodsItem> = useMemo(() => [ { title: '序号', key: 'index', align: 'center', width: 80, render: (_, __, index) => { const current = pagination.current || 1; const pageSize = pagination.pageSize || 10; return (current - 1) * pageSize + index + 1; }, }, { title: '商品图片', dataIndex: 'img', align: 'center', key: 'img', render: (img: string) => img ? ( <Image width={40} height={40} src={img} style={{ objectFit: 'cover', borderRadius: '4px' }} preview={{ minScale: 0.5, maxScale: 1.2, scaleStep: 0.1 }} /> ) : '无', }, { title: '商品名称', dataIndex: 'title', align: 'center', key: 'title', }, { title: '售价', dataIndex: 'price', align: 'center', key: 'price', render: (price: string) => `${price}` }, { title: '库存', dataIndex: 'stock', align: 'center', key: 'stock', }, { title: '销量', dataIndex: 'sold', align: 'center', key: 'sold', }, { title: '上下架', dataIndex: 'status', align: 'center', key: 'status', render: (status: number, record: GoodsItem) => ( <Switch checked={status === 1} checkedChildren="上架" unCheckedChildren="下架" loading={loadingStatus[record.id]} onChange={(checked) => handleStatusChange(record.id, checked)} /> ), }, { title: '排序', dataIndex: 'sort', align: 'center', key: 'sort', }, { title: '操作', key: 'action', width: 180, align: 'center', render: (_, record: GoodsItem) => ( <Space size="small"> <Button type="primary" icon={<EditOutlined />} className={styles.deleteButton} onClick={() => handleEdit(record)}> 编辑 </Button> <Popconfirm title="是否确认删除?" description="删除后不可恢复" onConfirm={() => handleDelete([record.id])} okText="确定" cancelText="取消" okType="danger" > <Button type="primary" danger className={styles.deleteButton} icon={<DeleteOutlined />}> 删除 </Button> </Popconfirm> </Space> ), }, ], [pagination.current, pagination.pageSize, loadingStatus, handleStatusChange, handleDelete, handleEdit]); // 列表选择项 const rowSelection = useMemo(() => ({ selectedRowKeys, onChange: setSelectedRowKeys, columnWidth: 30, }), [selectedRowKeys]); // 编辑页标签项 const tabItems = useMemo(() => [ { key: '1', label: '基础信息', children: ( <> <BaseInfoForm form={editForm} /> {renderActionButtons('1')} </> ), }, { key: '2', label: '规格库存', children: ( <> <SpecConfig form={editForm} specHook={specHook} /> {renderActionButtons('2')} </> ), }, { key: '3', label: '商品详情', children: ( <> <Form form={editForm} layout="horizontal" labelCol={{ span: 2 }} wrapperCol={{ span: 22 }}> <Form.Item name="content" label="商品详情" rules={createSimpleRules('商品详情')} > <UEditor ref={editor1} /> </Form.Item> </Form> {renderActionButtons('3')} </> ), }, { key: '4', label: '产品参数', children: ( <> <Form form={editForm} layout="horizontal" labelCol={{ span: 2 }} wrapperCol={{ span: 22 }}> <Form.Item name="params" label="产品参数" rules={createSimpleRules('产品参数')} > <UEditor ref={editor2} /> </Form.Item> </Form> {renderActionButtons('4')} </> ), }, { key: '5', label: '物流设置', children: ( <> <Form form={editForm} colon={false} layout="horizontal" labelCol={{ span: 2 }} wrapperCol={{ span: 22 }}> <Form.Item name="address" label="发货地址" rules={createSimpleRules('发货地址')} > <Input placeholder="请输入发货地址" className={styles.productNameInput} onChange={(e: React.ChangeEvent<HTMLInputElement>) => handleTextOnlyInput(editForm, 'address', e, { maxLength: 100 }) } /> </Form.Item> <Form.Item name="freight" label="运费" rules={createSimpleRules('运费')} > <Input placeholder="请输入运费" className={styles.productNameInput} onChange={(e: React.ChangeEvent<HTMLInputElement>) => handleNumberInput(editForm, 'freight', e, { supportDecimal: true }) } /> </Form.Item> </Form> {renderActionButtons('5')} </> ), }, { key: '6', label: '营销设置', children: ( <> <Form form={editForm} colon={false} layout="horizontal" labelCol={{ span: 2 }} wrapperCol={{ span: 22 }}> <Form.Item name="sort" label="排序" rules={createSimpleRules('排序')} > <Input placeholder="请输入排序" className={styles.productNameInput} onChange={(e: React.ChangeEvent<HTMLInputElement>) => handleNumberInput(editForm, 'sort', e, { supportDecimal: false }) } /> </Form.Item> <Form.Item name="sold" label="已售数量" rules={createSimpleRules('已售数量')} > <Input placeholder="请输入已售数量" className={styles.productNameInput} onChange={(e: React.ChangeEvent<HTMLInputElement>) => handleNumberInput(editForm, 'sold', e, { supportDecimal: false }) } /> </Form.Item> <Form.Item name="recommend" label="商品推荐" rules={createSimpleRules('商品推荐')} > <Checkbox.Group> <Checkbox value="1">热销商品</Checkbox> <Checkbox value="2">为您推荐</Checkbox> <Checkbox value="3">猜你喜欢</Checkbox> </Checkbox.Group> </Form.Item> <Form.Item name="keyword" label="关键字" rules={createSimpleRules('关键字')} > <Input placeholder="请输入关键字" className={styles.productNameInput} onChange={(e: React.ChangeEvent<HTMLInputElement>) => handleTextOnlyInput(editForm, 'keyword', e, { maxLength: 50 }) } /> </Form.Item> </Form> {renderActionButtons('6')} </> ), }, ], [editForm, renderActionButtons, specHook]); // 初始化获取商品列表 useEffect(() => { if (pageState === 'list') { getGoodsList(); } }, [pageState, getGoodsList]); // 分页变化时更新显示列表 useEffect(() => { handlePaginationChange(pagination.current || 1, pagination.pageSize || 10); }, [filteredGoodsList, pagination.current, pagination.pageSize, handlePaginationChange]); // 渲染页面 if (pageState === 'list') { return ( <div className={styles.pageWrapper}> <Title level={4} className={styles.pageTitle}>商品列表</Title> <div className={styles.recycleSearchContainer}> <Form form={searchForm} layout="inline" colon={false} labelAlign="left" className={styles.recycleSearchLeft}> <Row gutter={[24, 16]} style={{ width: '100%', marginBottom: 16 }}> <Col span={8}> <Form.Item name="title" label="商品名称" labelCol={{ span: 6 }} wrapperCol={{ span: 18 }}> <Input placeholder="请输入商品名称" onPressEnter={handleInputKeyPress} /> </Form.Item> </Col> <Col span={8}> <Form.Item name="keyword" label="关键字" labelCol={{ span: 6 }} wrapperCol={{ span: 18 }}> <Input placeholder="请输入关键字" onPressEnter={handleInputKeyPress} /> </Form.Item> </Col> <Col span={8}> <Form.Item name="goods_type_id" label="商品分类" labelCol={{ span: 6 }} wrapperCol={{ span: 18 }}> <Select placeholder="全部" allowClear onClick={handleSearch} onClear={handleSearch}> {goodsTypeList.map(option => ( <Option key={option.value} value={option.value}>{option.label}</Option> ))} </Select> </Form.Item> </Col> </Row> <Row gutter={[24, 16]} style={{ width: '100%' }}> <Col span={8}> <Form.Item name="synopsis" label="商品简介" labelCol={{ span: 6 }} wrapperCol={{ span: 18 }}> <Input placeholder="请输入商品简介" onPressEnter={handleInputKeyPress} /> </Form.Item> </Col> <Col span={8}> <Form.Item name="type" label="规格" labelCol={{ span: 6 }} wrapperCol={{ span: 18 }}> <Select placeholder="全部" allowClear onClick={handleSearch} onClear={handleSearch}> {SPECIFICATION_OPTIONS.map(option => ( <Option key={option.value} value={option.value}>{option.label}</Option> ))} </Select> </Form.Item> </Col> <Col span={8}> <Form.Item name="recommend" label="商品推荐" labelCol={{ span: 6 }} wrapperCol={{ span: 18 }}> <Select placeholder="全部" allowClear onClick={handleSearch} onClear={handleSearch}> {RECOMMEND_OPTIONS.map(option => ( <Option key={option.value} value={option.value}>{option.label}</Option> ))} </Select> </Form.Item> </Col> </Row> </Form> <div className={styles.recycleSearchRight}> <Button type="primary" icon={<SearchOutlined />} onClick={handleSearch} className={styles.recycleSearchButton}> 查询 </Button> <Button icon={<ReloadOutlined />} onClick={handleReset} className={styles.recycleResetButton}> 重置 </Button> </div> </div> <Card variant="borderless" styles={{ body: { padding: 0 } }}> <div className={styles.tabContent}> <div className={styles.actionBar}> <div className={styles.actionButtons}> <Space> <Button type="primary" className={styles.recycleDeleteButton} icon={<PlusOutlined />} onClick={handleAdd} > 新建 </Button> <Button danger icon={<DeleteOutlined />} onClick={handleBatchDelete} className={styles.recycleDeleteButton} disabled={selectedRowKeys.length === 0} > 批量删除 </Button> <Button type="primary" danger icon={<CloseOutlined />} className={styles.recycleDeleteButton} onClick={() => handleBatchStatusChange(true)} disabled={selectedRowKeys.length === 0} > 批量上架 </Button> </Space> </div> <div className={styles.refreshIcon}> <RedoOutlined style={{ fontSize: '18px', cursor: 'pointer' }} className={loading ? styles.refreshing : ''} title="刷新页面" onClick={handleRefresh} /> </div> </div> <Table columns={columns} dataSource={displayGoodsList} rowKey="id" loading={loading} className={styles.customTable} pagination={pagination} onChange={handleTableChange} scroll={{ x: 'max-content' }} rowSelection={rowSelection} /> </div> </Card> </div> ); } else { return ( <Card className={styles.cardContainer}> <div className={styles.headerBar}> <Button type="text" icon={<LeftOutlined className={styles.backIcon} />} onClick={handleCancel} className={styles.backButton} > 返回列表 </Button> <span className={styles.pageTitle}> {currentGoods ? '编辑商品' : '新建商品'} </span> </div> <Tabs activeKey={activeTab} items={tabItems} onChange={handleTabChange} size="large" className={styles.tabsContainer} /> </Card> ); } }; export default GoodsManagement;修复写出完整的
最新发布
11-23
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值