Initial commit
This commit is contained in:
25
.gitignore
vendored
Normal file
25
.gitignore
vendored
Normal file
@ -0,0 +1,25 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
package.lock.json
|
||||
pnpm-lock.yaml
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
31
.replit
Normal file
31
.replit
Normal file
@ -0,0 +1,31 @@
|
||||
run = "npm run dev"
|
||||
entrypoint = "src/App.jsx"
|
||||
|
||||
hidden = [".config", "tsconfig.json", "tsconfig.node.json", "vite.config.js", ".gitignore"]
|
||||
|
||||
[nix]
|
||||
channel = "stable-22_11"
|
||||
|
||||
[env]
|
||||
PATH = "/home/runner/$REPL_SLUG/.config/npm/node_global/bin:/home/runner/$REPL_SLUG/node_modules/.bin"
|
||||
npm_config_prefix = "/home/runner/$REPL_SLUG/.config/npm/node_global"
|
||||
|
||||
[gitHubImport]
|
||||
requiredFiles = [".replit", "replit.nix", ".config"]
|
||||
|
||||
[packager]
|
||||
language = "nodejs"
|
||||
[packager.features]
|
||||
packageSearch = true
|
||||
guessImports = true
|
||||
enabledForHosting = false
|
||||
|
||||
[languages]
|
||||
[languages.javascript]
|
||||
pattern = "**/{*.js,*.jsx,*.ts,*.tsx}"
|
||||
[languages.javascript.languageServer]
|
||||
start = "typescript-language-server --stdio"
|
||||
|
||||
[deployment]
|
||||
build = ["sh", "-c", "npm run build"]
|
||||
run = ["sh", "-c", "npm run preview"]
|
21
README.md
Normal file
21
README.md
Normal file
@ -0,0 +1,21 @@
|
||||
# Getting Started
|
||||
- Hit run
|
||||
- Edit [App.tsx](#src/App.tsx) and watch it live update!
|
||||
|
||||
# Learn More
|
||||
|
||||
You can learn more in the [Base Extension Development Guide](https://lark-technologies.larksuite.com/docx/HvCbdSzXNowzMmxWgXsuB2Ngs7d) or [多维表格扩展脚本开发指南](https://feishu.feishu.cn/docx/U3wodO5eqome3uxFAC3cl0qanIe).
|
||||
|
||||
## Install packages
|
||||
|
||||
Install packages in Shell pane or search and add in Packages pane.
|
||||
|
||||
## Publish
|
||||
Please npm run build first, submit it together with the dist directory, and then fill in the form:
|
||||
[Share form](https://feishu.feishu.cn/share/base/form/shrcnGFgOOsFGew3SDZHPhzkM0e)
|
||||
|
||||
|
||||
|
||||
## 发布
|
||||
请先npm run build,连同dist目录一起提交,然后再填写表单:
|
||||
[共享表单](https://feishu.feishu.cn/share/base/form/shrcnGFgOOsFGew3SDZHPhzkM0e)
|
1086
dist/assets/RenderMarkDown-B3QOZGRQ.aa8b7ecb.js
vendored
Normal file
1086
dist/assets/RenderMarkDown-B3QOZGRQ.aa8b7ecb.js
vendored
Normal file
File diff suppressed because one or more lines are too long
468
dist/assets/index.fcb5e076.js
vendored
Normal file
468
dist/assets/index.fcb5e076.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
dist/assets/index.ffc31103.css
vendored
Normal file
1
dist/assets/index.ffc31103.css
vendored
Normal file
File diff suppressed because one or more lines are too long
15
dist/favicon.svg
vendored
Normal file
15
dist/favicon.svg
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
<svg width="410" height="404" viewBox="0 0 410 404" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M399.641 59.5246L215.643 388.545C211.844 395.338 202.084 395.378 198.228 388.618L10.5817 59.5563C6.38087 52.1896 12.6802 43.2665 21.0281 44.7586L205.223 77.6824C206.398 77.8924 207.601 77.8904 208.776 77.6763L389.119 44.8058C397.439 43.2894 403.768 52.1434 399.641 59.5246Z" fill="url(#paint0_linear)"/>
|
||||
<path d="M292.965 1.5744L156.801 28.2552C154.563 28.6937 152.906 30.5903 152.771 32.8664L144.395 174.33C144.198 177.662 147.258 180.248 150.51 179.498L188.42 170.749C191.967 169.931 195.172 173.055 194.443 176.622L183.18 231.775C182.422 235.487 185.907 238.661 189.532 237.56L212.947 230.446C216.577 229.344 220.065 232.527 219.297 236.242L201.398 322.875C200.278 328.294 207.486 331.249 210.492 326.603L212.5 323.5L323.454 102.072C325.312 98.3645 322.108 94.137 318.036 94.9228L279.014 102.454C275.347 103.161 272.227 99.746 273.262 96.1583L298.731 7.86689C299.767 4.27314 296.636 0.855181 292.965 1.5744Z" fill="url(#paint1_linear)"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear" x1="6.00017" y1="32.9999" x2="235" y2="344" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#41D1FF"/>
|
||||
<stop offset="1" stop-color="#BD34FE"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear" x1="194.651" y1="8.81818" x2="236.076" y2="292.989" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FFEA83"/>
|
||||
<stop offset="0.0833333" stop-color="#FFDD35"/>
|
||||
<stop offset="1" stop-color="#FFA800"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 1.5 KiB |
15
dist/index.html
vendored
Normal file
15
dist/index.html
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="./favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Base Script</title>
|
||||
<script type="module" crossorigin src="./assets/index.fcb5e076.js"></script>
|
||||
<link rel="stylesheet" href="./assets/index.ffc31103.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
</body>
|
||||
</html>
|
13
index.html
Normal file
13
index.html
Normal file
@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Base Script</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/index.tsx"></script>
|
||||
</body>
|
||||
</html>
|
33
package.json
Normal file
33
package.json
Normal file
@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "react-emplate",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"description": "React TypeScript on Replit, using Vite bundler",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"keywords": [],
|
||||
"output": "dist",
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.0.15",
|
||||
"@types/react-dom": "^18.0.6",
|
||||
"@vitejs/plugin-react": "^2.0.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"typescript": "^4.7.4",
|
||||
"vite": "^3.0.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@douyinfe/semi-foundation": "^2.38.0",
|
||||
"@douyinfe/semi-ui": "^2.36.0",
|
||||
"@lark-base-open/js-sdk": "^0.5.0",
|
||||
"i18next": "^23.5.1",
|
||||
"i18next-browser-languagedetector": "^7.1.0",
|
||||
"react-i18next": "^13.2.2",
|
||||
"reset-css": "^5.0.1"
|
||||
}
|
||||
}
|
15
public/favicon.svg
Normal file
15
public/favicon.svg
Normal file
@ -0,0 +1,15 @@
|
||||
<svg width="410" height="404" viewBox="0 0 410 404" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M399.641 59.5246L215.643 388.545C211.844 395.338 202.084 395.378 198.228 388.618L10.5817 59.5563C6.38087 52.1896 12.6802 43.2665 21.0281 44.7586L205.223 77.6824C206.398 77.8924 207.601 77.8904 208.776 77.6763L389.119 44.8058C397.439 43.2894 403.768 52.1434 399.641 59.5246Z" fill="url(#paint0_linear)"/>
|
||||
<path d="M292.965 1.5744L156.801 28.2552C154.563 28.6937 152.906 30.5903 152.771 32.8664L144.395 174.33C144.198 177.662 147.258 180.248 150.51 179.498L188.42 170.749C191.967 169.931 195.172 173.055 194.443 176.622L183.18 231.775C182.422 235.487 185.907 238.661 189.532 237.56L212.947 230.446C216.577 229.344 220.065 232.527 219.297 236.242L201.398 322.875C200.278 328.294 207.486 331.249 210.492 326.603L212.5 323.5L323.454 102.072C325.312 98.3645 322.108 94.137 318.036 94.9228L279.014 102.454C275.347 103.161 272.227 99.746 273.262 96.1583L298.731 7.86689C299.767 4.27314 296.636 0.855181 292.965 1.5744Z" fill="url(#paint1_linear)"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear" x1="6.00017" y1="32.9999" x2="235" y2="344" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#41D1FF"/>
|
||||
<stop offset="1" stop-color="#BD34FE"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear" x1="194.651" y1="8.81818" x2="236.076" y2="292.989" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FFEA83"/>
|
||||
<stop offset="0.0833333" stop-color="#FFDD35"/>
|
||||
<stop offset="1" stop-color="#FFA800"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 1.5 KiB |
8
replit.nix
Normal file
8
replit.nix
Normal file
@ -0,0 +1,8 @@
|
||||
{ pkgs }: {
|
||||
deps = [
|
||||
pkgs.nodejs-16_x
|
||||
pkgs.nodePackages.typescript-language-server
|
||||
pkgs.yarn
|
||||
pkgs.replitPackages.jest
|
||||
];
|
||||
}
|
12
src/App.css
Normal file
12
src/App.css
Normal file
@ -0,0 +1,12 @@
|
||||
@import 'reset-css';
|
||||
|
||||
.main {
|
||||
padding: 1.5rem 1rem 1rem;
|
||||
}
|
||||
.main h4 {
|
||||
font-size: calc(1.275rem + 0.3vw);
|
||||
}
|
||||
.main code {
|
||||
color: #d63384;
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace;
|
||||
}
|
501
src/App.tsx
Normal file
501
src/App.tsx
Normal file
@ -0,0 +1,501 @@
|
||||
import React, { useState, useEffect, useMemo, useCallback, memo } from 'react';
|
||||
import { bitable, CurrencyCode, FieldType, ICurrencyField, ICurrencyFieldMeta } from '@lark-base-open/js-sdk';
|
||||
import { Card, Modal, Checkbox, message } from 'antd';
|
||||
import './App.css';
|
||||
|
||||
// 选项类型定义
|
||||
interface OptionGroupDef {
|
||||
title: string;
|
||||
options: string[];
|
||||
required: boolean;
|
||||
level?: number;
|
||||
parentOption?: string;
|
||||
condition?: (checkedList: string[]) => boolean;
|
||||
resetOn?: string[];
|
||||
}
|
||||
|
||||
// 选项分组配置
|
||||
const OPTION_GROUPS: OptionGroupDef[] = [
|
||||
{
|
||||
title: '单据类型',
|
||||
options: ['首单', '翻单'],
|
||||
required: true,
|
||||
level: 1
|
||||
},
|
||||
{
|
||||
title: '是否要打板',
|
||||
options: ['需要打板', '不需要打板'],
|
||||
required: false,
|
||||
level: 2,
|
||||
parentOption: '首单',
|
||||
condition: (checkedList) => checkedList.includes('首单'),
|
||||
resetOn: ['翻单']
|
||||
},
|
||||
{
|
||||
title: '翻单变动',
|
||||
options: ['无变动不需要修改', '有变动需要修改'],
|
||||
required: false,
|
||||
level: 2,
|
||||
parentOption: '翻单',
|
||||
condition: (checkedList) => checkedList.includes('翻单'),
|
||||
resetOn: ['首单']
|
||||
},
|
||||
{
|
||||
title: '特殊订单',
|
||||
options: ['换料寄面料样', '换料重新打板', '加色', '改尺寸不打版', '改尺寸重新打板'],
|
||||
required: false,
|
||||
level: 3,
|
||||
parentOption: '有变动需要修改',
|
||||
condition: (checkedList) => checkedList.includes('有变动需要修改'),
|
||||
resetOn: ['首单', '无变动不需要修改']
|
||||
},
|
||||
{
|
||||
title: '批色样',
|
||||
options: ['要批色样', '不要批色样'],
|
||||
required: true,
|
||||
level: 4,
|
||||
parentOption: '加色',
|
||||
condition: (checkedList) => checkedList.includes('加色'),
|
||||
resetOn: ['首单', '无变动不需要修改', '需要打板', '不需要打板']
|
||||
},
|
||||
{
|
||||
title: '批色样',
|
||||
options: ['要批色样', '不要批色样'],
|
||||
required: true,
|
||||
level: 3,
|
||||
parentOption: '不需要打板',
|
||||
condition: (checkedList) => checkedList.includes('首单') && checkedList.includes('不需要打板'),
|
||||
resetOn: ['翻单', '有变动需要修改', '无变动不需要修改']
|
||||
},
|
||||
{
|
||||
title: '品类',
|
||||
options: ['牛仔', '时装'],
|
||||
required: true
|
||||
},
|
||||
{
|
||||
title: '复杂度',
|
||||
options: ['简单款', '基础款', '复杂款'],
|
||||
required: true
|
||||
},
|
||||
{
|
||||
title: '二次工艺',
|
||||
options: ['绣花', '印花'],
|
||||
required: false
|
||||
}
|
||||
];
|
||||
|
||||
const FABRIC_TEST_OPTIONS = ['需要面料测试', '不需要面料测试'];
|
||||
|
||||
// 优化:预计算选项集合,避免重复includes操作
|
||||
const createOptionSets = (checkedList: string[]) => {
|
||||
const checkedSet = new Set(checkedList);
|
||||
return {
|
||||
checkedSet,
|
||||
hasFirstOrder: checkedSet.has('首单'),
|
||||
hasReorder: checkedSet.has('翻单'),
|
||||
hasNoChange: checkedSet.has('无变动不需要修改'),
|
||||
hasChange: checkedSet.has('有变动需要修改'),
|
||||
hasAddColor: checkedSet.has('加色'),
|
||||
hasNoPlate: checkedSet.has('不需要打板')
|
||||
};
|
||||
};
|
||||
|
||||
// 优化:使用memo包装OptionGroup组件
|
||||
interface OptionGroupProps {
|
||||
group: OptionGroupDef;
|
||||
checkedList: string[];
|
||||
onChange: (newList: string[]) => void;
|
||||
lockedOptions?: string[];
|
||||
level?: number;
|
||||
}
|
||||
|
||||
const OptionGroup = memo<OptionGroupProps>(({
|
||||
group,
|
||||
checkedList,
|
||||
onChange,
|
||||
lockedOptions = [],
|
||||
level = 1
|
||||
}) => {
|
||||
const isMulti = group.title === '二次工艺';
|
||||
|
||||
// 优化:使用useMemo缓存计算结果
|
||||
const groupChecked = useMemo(() =>
|
||||
checkedList.filter(v => group.options.includes(v)),
|
||||
[checkedList, group.options]
|
||||
);
|
||||
|
||||
const indentStyle = useMemo(() => ({
|
||||
marginLeft: level > 1 ? (level - 1) * 24 : 0,
|
||||
marginTop: level > 1 ? 8 : 0,
|
||||
borderLeft: level > 1 ? `${level > 2 ? 'dashed' : 'solid'} 2px #eee` : 'none',
|
||||
paddingLeft: level > 1 ? 12 : 0
|
||||
}), [level]);
|
||||
|
||||
const titleColor = useMemo(() => {
|
||||
switch(level) {
|
||||
case 1: return '#000';
|
||||
case 2: return '#888';
|
||||
case 3: return '#b36d00';
|
||||
default: return '#888';
|
||||
}
|
||||
}, [level]);
|
||||
|
||||
const options = useMemo(() =>
|
||||
group.options.map(opt => ({
|
||||
label: opt,
|
||||
value: opt,
|
||||
disabled: lockedOptions.includes(opt)
|
||||
})),
|
||||
[group.options, lockedOptions]
|
||||
);
|
||||
|
||||
const handleChange = useCallback((list: string[]) => {
|
||||
const others = checkedList.filter(v => !group.options.includes(v));
|
||||
let newList;
|
||||
if (isMulti) {
|
||||
newList = [...others, ...list];
|
||||
} else {
|
||||
newList = [...others, list.slice(-1)[0]].filter(Boolean);
|
||||
}
|
||||
onChange(newList);
|
||||
}, [checkedList, group.options, isMulti, onChange]);
|
||||
|
||||
return (
|
||||
<div key={group.title} style={{ ...indentStyle, marginBottom: 12 }}>
|
||||
<div style={{ fontWeight: 'bold', marginBottom: 4, color: titleColor }}>
|
||||
{group.title}
|
||||
{group.required && <span style={{ color: 'red', marginLeft: 4 }}>*</span>}
|
||||
</div>
|
||||
<Checkbox.Group
|
||||
options={options}
|
||||
value={groupChecked}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
// 优化:使用memo包装OrderConfigSelector组件
|
||||
interface OrderConfigSelectorProps {
|
||||
selectedRecordId: string | null;
|
||||
checkedList: string[];
|
||||
setCheckedList: (list: string[]) => void;
|
||||
onSubmit: (options: string[]) => void;
|
||||
onCancel: () => void;
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
const OrderConfigSelector = memo<OrderConfigSelectorProps>(({
|
||||
selectedRecordId,
|
||||
checkedList,
|
||||
setCheckedList,
|
||||
onSubmit,
|
||||
onCancel,
|
||||
loading
|
||||
}) => {
|
||||
// 优化:使用useMemo缓存可见的选项组
|
||||
const visibleGroups = useMemo(() => {
|
||||
return OPTION_GROUPS.filter(group => {
|
||||
if (!group.condition) return true;
|
||||
return group.condition(checkedList);
|
||||
});
|
||||
}, [checkedList]);
|
||||
|
||||
// 优化:使用useCallback缓存事件处理函数
|
||||
const handleCheckedListChange = useCallback((newList: string[]) => {
|
||||
const addedOptions = newList.filter(opt => !checkedList.includes(opt));
|
||||
|
||||
if (addedOptions.length > 0) {
|
||||
const resetGroups = OPTION_GROUPS.filter(group =>
|
||||
group.resetOn?.some(resetOpt => addedOptions.includes(resetOpt))
|
||||
);
|
||||
|
||||
if (resetGroups.length > 0) {
|
||||
const resetGroupsSet = new Set(resetGroups);
|
||||
const filteredOptions = newList.filter(opt => {
|
||||
const group = OPTION_GROUPS.find(g => g.options.includes(opt));
|
||||
return !resetGroupsSet.has(group as OptionGroupDef);
|
||||
});
|
||||
setCheckedList(filteredOptions);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setCheckedList(newList);
|
||||
}, [checkedList, setCheckedList]);
|
||||
|
||||
// 优化:使用useCallback缓存验证函数
|
||||
const validateRequired = useCallback(() => {
|
||||
for (const group of OPTION_GROUPS) {
|
||||
if (group.required && group.condition?.(checkedList) !== false) {
|
||||
const has = checkedList.some(v => group.options.includes(v));
|
||||
if (!has) {
|
||||
message.error(`请至少选择一项【${group.title}】`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 特殊验证:如果选择了"不需要打板",必须选择是否批色样
|
||||
if (checkedList.includes('首单') && checkedList.includes('不需要打板')) {
|
||||
const hasColorSample = checkedList.some(v => v === '要批色样' || v === '不要批色样');
|
||||
if (!hasColorSample) {
|
||||
message.error('请选择是否要批色样');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}, [checkedList]);
|
||||
|
||||
const needFabricTestDialog = useCallback(() => {
|
||||
return !(checkedList.includes('翻单') && checkedList.includes('无变动不需要修改'));
|
||||
}, [checkedList]);
|
||||
|
||||
const [showFabricTestModal, setShowFabricTestModal] = useState(false);
|
||||
const [fabricTestSelection, setFabricTestSelection] = useState<string[]>([]);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [finalOptions, setFinalOptions] = useState<string[]>([]);
|
||||
|
||||
const handleSubmit = useCallback(() => {
|
||||
if (!validateRequired()) return;
|
||||
|
||||
setFinalOptions([...checkedList]);
|
||||
|
||||
if (needFabricTestDialog()) {
|
||||
setFabricTestSelection([]);
|
||||
setShowFabricTestModal(true);
|
||||
} else {
|
||||
onSubmit([...checkedList]);
|
||||
}
|
||||
}, [validateRequired, checkedList, needFabricTestDialog, onSubmit]);
|
||||
|
||||
const handleFabricTestSubmit = useCallback(() => {
|
||||
if (fabricTestSelection.length === 0) {
|
||||
message.error('请选择是否需要面料测试');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSubmitting(true);
|
||||
onSubmit([...finalOptions, ...fabricTestSelection]);
|
||||
setShowFabricTestModal(false);
|
||||
}, [fabricTestSelection, finalOptions, onSubmit]);
|
||||
|
||||
const handleCancel = useCallback(() => {
|
||||
onCancel();
|
||||
setCheckedList([]);
|
||||
}, [onCancel, setCheckedList]);
|
||||
|
||||
const handleFabricTestChange = useCallback((values: string[]) => {
|
||||
if (values.length > 0) {
|
||||
setFabricTestSelection([values[values.length - 1]]);
|
||||
} else {
|
||||
setFabricTestSelection([]);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!showFabricTestModal && isSubmitting) {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
}, [showFabricTestModal, isSubmitting]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
title="订单配置"
|
||||
open={!!selectedRecordId}
|
||||
onOk={handleSubmit}
|
||||
onCancel={handleCancel}
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
confirmLoading={loading}
|
||||
>
|
||||
{visibleGroups.map(group => (
|
||||
<OptionGroup
|
||||
key={group.title}
|
||||
group={group}
|
||||
checkedList={checkedList}
|
||||
onChange={handleCheckedListChange}
|
||||
level={group.level}
|
||||
/>
|
||||
))}
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
title="面料测试"
|
||||
open={showFabricTestModal}
|
||||
onOk={handleFabricTestSubmit}
|
||||
onCancel={() => setShowFabricTestModal(false)}
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
confirmLoading={isSubmitting}
|
||||
>
|
||||
<div>
|
||||
<p>请选择是否需要面料测试:</p>
|
||||
<Checkbox.Group
|
||||
options={FABRIC_TEST_OPTIONS}
|
||||
value={fabricTestSelection}
|
||||
onChange={handleFabricTestChange}
|
||||
/>
|
||||
</div>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
// 优化:主应用组件
|
||||
const App: React.FC = () => {
|
||||
const [selectedRecordId, setSelectedRecordId] = useState<string | null>(null);
|
||||
const [checkedList, setCheckedList] = useState<string[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [currentSelection, setCurrentSelection] = useState<{
|
||||
tableId: string | null;
|
||||
recordId: string | null;
|
||||
}>({ tableId: null, recordId: null });
|
||||
|
||||
// 优化:缓存字段选项映射,避免重复查找
|
||||
const [fieldOptionsMap, setFieldOptionsMap] = useState<Map<string, {id: string, name: string}>>(new Map());
|
||||
|
||||
// 优化:使用useCallback缓存函数
|
||||
const resetOptions = useCallback(() => {
|
||||
setSelectedRecordId(null);
|
||||
setCheckedList([]);
|
||||
}, []);
|
||||
|
||||
const saveOptions = useCallback(async () => {
|
||||
if (!selectedRecordId || !currentSelection.tableId) {
|
||||
message.warning('没有有效的选中记录或表格信息');
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
const FIELD_ID_TO_SAVE = 'fldTtRHwlo';
|
||||
|
||||
try {
|
||||
const table = await bitable.base.getTableById(currentSelection.tableId);
|
||||
if (!table) {
|
||||
message.error('无法获取表格对象');
|
||||
return;
|
||||
}
|
||||
|
||||
const field = await table.getFieldById(FIELD_ID_TO_SAVE);
|
||||
if (!field) {
|
||||
message.error('无法获取字段对象');
|
||||
return;
|
||||
}
|
||||
|
||||
// 优化:只在字段选项映射为空时才重新获取
|
||||
let optionsMap = fieldOptionsMap;
|
||||
if (optionsMap.size === 0) {
|
||||
const fieldMeta = await field.getMeta();
|
||||
console.log('字段元数据:', fieldMeta);
|
||||
const fieldOptions = fieldMeta.property?.options || [];
|
||||
optionsMap = new Map(fieldOptions.map(opt => [opt.name, opt]));
|
||||
setFieldOptionsMap(optionsMap);
|
||||
}
|
||||
|
||||
let dataToSave = null;
|
||||
if (checkedList.length > 0) {
|
||||
console.log('字段中的所有选项:', Array.from(optionsMap.keys()));
|
||||
console.log('要保存的选项:', checkedList);
|
||||
|
||||
// 优化:使用Map查找,O(1)时间复杂度
|
||||
dataToSave = checkedList.map(optionText => {
|
||||
const matchedOption = optionsMap.get(optionText);
|
||||
if (!matchedOption) {
|
||||
console.warn(`未找到选项 "${optionText}" 对应的ID`);
|
||||
console.log('可能的匹配选项:', Array.from(optionsMap.keys()).filter(key =>
|
||||
key.includes(optionText) || optionText.includes(key)
|
||||
));
|
||||
return null;
|
||||
}
|
||||
console.log(`找到匹配: "${optionText}" -> ID: ${matchedOption.id}`);
|
||||
return {
|
||||
id: matchedOption.id,
|
||||
text: matchedOption.name
|
||||
};
|
||||
}).filter(Boolean);
|
||||
}
|
||||
|
||||
console.log('准备保存的数据:', dataToSave);
|
||||
await field.setValue(selectedRecordId, dataToSave);
|
||||
|
||||
const savedValue = await field.getValue(selectedRecordId);
|
||||
console.log('保存后的字段值:', savedValue);
|
||||
|
||||
if (savedValue) {
|
||||
console.log('数据成功写入单元格');
|
||||
message.success('已保存选项');
|
||||
} else {
|
||||
throw new Error('数据写入后未能读取到值');
|
||||
}
|
||||
|
||||
} catch (e: any) {
|
||||
console.error('保存失败:', e);
|
||||
message.error(`保存失败: ${e.message || '未知错误'}`);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
resetOptions();
|
||||
setCurrentSelection({ tableId: null, recordId: null });
|
||||
}
|
||||
}, [selectedRecordId, currentSelection.tableId, checkedList, fieldOptionsMap, resetOptions]);
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = bitable.base.onSelectionChange(async (event: any) => {
|
||||
try {
|
||||
const { data } = event;
|
||||
if (data && data.tableId && data.recordId) {
|
||||
const isNewCell = (
|
||||
data.tableId !== currentSelection.tableId ||
|
||||
data.recordId !== currentSelection.recordId
|
||||
);
|
||||
|
||||
if (isNewCell) {
|
||||
resetOptions();
|
||||
setCurrentSelection({
|
||||
tableId: data.tableId,
|
||||
recordId: data.recordId
|
||||
});
|
||||
// 优化:切换单元格时清空字段选项缓存
|
||||
setFieldOptionsMap(new Map());
|
||||
}
|
||||
|
||||
const table = await bitable.base.getTableById(data.tableId);
|
||||
const cellValue = await table.getCellValue('fldTtRHwlo', data.recordId);
|
||||
setSelectedRecordId(data.recordId);
|
||||
|
||||
if (typeof cellValue === 'string' && cellValue.trim() !== '') {
|
||||
setCheckedList(cellValue.split(',').map(item => item.trim()));
|
||||
} else if (Array.isArray(cellValue)) {
|
||||
setCheckedList(cellValue.map(item => String(item)));
|
||||
} else {
|
||||
setCheckedList([]);
|
||||
}
|
||||
}
|
||||
} catch (e: any) {
|
||||
message.error('获取选中记录失败: ' + (e.message || e));
|
||||
}
|
||||
});
|
||||
return unsubscribe;
|
||||
}, [currentSelection, resetOptions]);
|
||||
|
||||
return (
|
||||
<div style={{ padding: '16px' }}>
|
||||
<Card>
|
||||
<div>请先在多维表格中点击一个单元格以加载配置。</div>
|
||||
</Card>
|
||||
|
||||
<OrderConfigSelector
|
||||
selectedRecordId={selectedRecordId}
|
||||
checkedList={checkedList}
|
||||
setCheckedList={setCheckedList}
|
||||
onSubmit={saveOptions}
|
||||
onCancel={resetOptions}
|
||||
loading={loading}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
37
src/components/LoadApp/index.tsx
Normal file
37
src/components/LoadApp/index.tsx
Normal file
@ -0,0 +1,37 @@
|
||||
import { ReactElement, useEffect, useState } from "react"
|
||||
import { bitable } from "@lark-base-open/js-sdk"
|
||||
import './style.css'
|
||||
|
||||
export default function LoadApp(props: { neverShowBanner?: boolean, children: ReactElement }): ReactElement {
|
||||
const [loadErr, setLoadErr] = useState(false)
|
||||
|
||||
const TopBanner = <div>
|
||||
<div className='errTop'>
|
||||
After running the project, please get the webview address and paste it into the Base table "Extended Script" for use. See:
|
||||
<a target='_blank' href='https://bytedance.feishu.cn/docx/HazFdSHH9ofRGKx8424cwzLlnZc'>Development Guide</a>
|
||||
</div>
|
||||
</div>
|
||||
useEffect(() => {
|
||||
if (props.neverShowBanner) return;
|
||||
const timer = new Promise((resolve, reject) => {
|
||||
setTimeout(() => {
|
||||
reject(false)
|
||||
}, 3000)
|
||||
})
|
||||
Promise.race([bitable.bridge.getLanguage(), timer]).then((v) => {
|
||||
setLoadErr(false)
|
||||
}).catch(() => {
|
||||
setLoadErr(true)
|
||||
})
|
||||
}, [])
|
||||
|
||||
if (props.neverShowBanner) {
|
||||
return props.children || null
|
||||
}
|
||||
|
||||
return <div>
|
||||
{loadErr && TopBanner}
|
||||
{props.children}
|
||||
</div>
|
||||
}
|
||||
|
6
src/components/LoadApp/style.css
Normal file
6
src/components/LoadApp/style.css
Normal file
@ -0,0 +1,6 @@
|
||||
.errTop{
|
||||
padding: 14px;
|
||||
background: #fffbe6;
|
||||
border: 1px solid #ffe58f;
|
||||
border-radius: 8px
|
||||
}
|
8
src/index.tsx
Normal file
8
src/index.tsx
Normal file
@ -0,0 +1,8 @@
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import './App.css';
|
||||
import App from './App';
|
||||
import LoadApp from './components/LoadApp';
|
||||
// import './locales/i18n' // 支持国际化
|
||||
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(<LoadApp ><App /></LoadApp>)
|
||||
|
||||
|
3
src/locales/en.json
Normal file
3
src/locales/en.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"title":"这是英文标题"
|
||||
}
|
34
src/locales/i18n.ts
Normal file
34
src/locales/i18n.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import i18n from 'i18next';
|
||||
import { initReactI18next } from 'react-i18next';
|
||||
import LanguageDetector from 'i18next-browser-languagedetector';
|
||||
import { bitable } from '@lark-base-open/js-sdk';
|
||||
import translationEN from './en.json';
|
||||
import translationZH from './zh.json';
|
||||
|
||||
const resources = {
|
||||
zh: {
|
||||
translation: translationZH,
|
||||
},
|
||||
en: {
|
||||
translation: translationEN,
|
||||
},
|
||||
};
|
||||
|
||||
i18n
|
||||
.use(LanguageDetector)
|
||||
.use(initReactI18next)
|
||||
.init({
|
||||
resources,
|
||||
fallbackLng: 'en', // 指定降级文案为英文
|
||||
interpolation: {
|
||||
escapeValue: false,
|
||||
},
|
||||
});
|
||||
|
||||
bitable.bridge.getLanguage().then((lng) => {
|
||||
if (i18n.language !== lng) {
|
||||
i18n.changeLanguage(lng);
|
||||
}
|
||||
});
|
||||
|
||||
export default i18n;
|
3
src/locales/zh.json
Normal file
3
src/locales/zh.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"title":"这是中文标题"
|
||||
}
|
21
tsconfig.json
Normal file
21
tsconfig.json
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
||||
"allowJs": false,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": false,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx"
|
||||
},
|
||||
"include": ["src"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
9
tsconfig.node.json
Normal file
9
tsconfig.node.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
16
vite.config.js
Normal file
16
vite.config.js
Normal file
@ -0,0 +1,16 @@
|
||||
import { defineConfig } from "vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
base: "./",
|
||||
plugins: [react()],
|
||||
server: {
|
||||
host: "0.0.0.0",
|
||||
},
|
||||
build: {
|
||||
rollupOptions: {
|
||||
external: ["#minpath", "#minproc", "#minurl"],
|
||||
},
|
||||
},
|
||||
});
|
Reference in New Issue
Block a user