Initial commit

This commit is contained in:
ruimingMai
2025-06-17 19:07:13 +08:00
commit fba0a30a8d
23 changed files with 2381 additions and 0 deletions

25
.gitignore vendored Normal file
View 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
View 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
View 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)

File diff suppressed because one or more lines are too long

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

File diff suppressed because one or more lines are too long

15
dist/favicon.svg vendored Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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;

View 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:&nbsp;
<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>
}

View File

@ -0,0 +1,6 @@
.errTop{
padding: 14px;
background: #fffbe6;
border: 1px solid #ffe58f;
border-radius: 8px
}

8
src/index.tsx Normal file
View 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
View File

@ -0,0 +1,3 @@
{
"title":"这是英文标题"
}

34
src/locales/i18n.ts Normal file
View 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
View File

@ -0,0 +1,3 @@
{
"title":"这是中文标题"
}

21
tsconfig.json Normal file
View 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
View File

@ -0,0 +1,9 @@
{
"compilerOptions": {
"composite": true,
"module": "ESNext",
"moduleResolution": "Node",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}

16
vite.config.js Normal file
View 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"],
},
},
});