初始化
This commit is contained in:
173
README.md
Normal file
173
README.md
Normal file
@@ -0,0 +1,173 @@
|
||||
# 飞书审批配置服务
|
||||
|
||||
内网 Web 服务:用户在浏览器中设计审批表单与流程,保存时**下载包含全部配置的独立 HTML 文件**,可离线打开或分发给他人。
|
||||
|
||||
## 功能
|
||||
|
||||
- 在线编辑:基础信息、表单设计、流程设计
|
||||
- **保存并下载 HTML**:配置写入 HTML 文件,无需本地 bat / data 文件夹
|
||||
- **导入 HTML**:继续编辑之前导出的配置文件
|
||||
|
||||
## 环境要求
|
||||
|
||||
- **Node.js 16.x**(CentOS 7 推荐 16.20 LTS)
|
||||
- 无需 `npm install`(零第三方依赖,仅使用 Node 内置模块)
|
||||
|
||||
## 快速启动
|
||||
|
||||
```bash
|
||||
cd Approval_of_design
|
||||
node server.js
|
||||
```
|
||||
|
||||
浏览器访问:`http://服务器IP:8080`
|
||||
|
||||
端口与监听地址可在 `project.config.json` 中修改:
|
||||
|
||||
```json
|
||||
{
|
||||
"projectId": "approval_of_design",
|
||||
"port": 8080,
|
||||
"host": "0.0.0.0",
|
||||
"label": "飞书审批配置服务"
|
||||
}
|
||||
```
|
||||
|
||||
## CentOS 7 部署
|
||||
|
||||
### 1. 安装 Node.js 16.x
|
||||
|
||||
CentOS 7 自带 glibc 较旧,建议使用 NodeSource 16.x 或 nvm:
|
||||
|
||||
**方式 A:NodeSource(推荐)**
|
||||
|
||||
```bash
|
||||
curl -fsSL https://rpm.nodesource.com/setup_16.x | sudo bash -
|
||||
sudo yum install -y nodejs
|
||||
node -v # 应显示 v16.x.x
|
||||
```
|
||||
|
||||
**方式 B:nvm**
|
||||
|
||||
```bash
|
||||
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
|
||||
source ~/.bashrc
|
||||
nvm install 16
|
||||
nvm use 16
|
||||
```
|
||||
|
||||
### 2. 上传并启动
|
||||
|
||||
```bash
|
||||
sudo mkdir -p /opt/feishu-approval
|
||||
# 将项目文件上传到 /opt/feishu-approval
|
||||
cd /opt/feishu-approval
|
||||
node server.js
|
||||
```
|
||||
|
||||
或使用项目自带脚本:
|
||||
|
||||
```bash
|
||||
chmod +x deploy/start.sh
|
||||
./deploy/start.sh
|
||||
```
|
||||
|
||||
### 3. systemd 开机自启(推荐)
|
||||
|
||||
```bash
|
||||
# 修改 deploy/feishu-approval.service 中 WorkingDirectory、User 后:
|
||||
sudo cp deploy/feishu-approval.service /etc/systemd/system/
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable feishu-approval
|
||||
sudo systemctl start feishu-approval
|
||||
sudo systemctl status feishu-approval
|
||||
```
|
||||
|
||||
### 4. 防火墙
|
||||
|
||||
```bash
|
||||
sudo firewall-cmd --permanent --add-port=8080/tcp
|
||||
sudo firewall-cmd --reload
|
||||
```
|
||||
|
||||
### 5. 验证
|
||||
|
||||
```bash
|
||||
curl http://127.0.0.1:8080/api/health
|
||||
```
|
||||
|
||||
## 部署到其他环境
|
||||
|
||||
### 1. 上传项目
|
||||
|
||||
将整个 `Approval_of_design` 目录复制到内网服务器。
|
||||
|
||||
### 2. 启动服务
|
||||
|
||||
**方式 A:直接运行**
|
||||
|
||||
```bash
|
||||
node server.js
|
||||
```
|
||||
|
||||
**方式 B:使用 pm2 守护进程(推荐)**
|
||||
|
||||
```bash
|
||||
npm install -g pm2
|
||||
pm2 start server.js --name feishu-approval
|
||||
pm2 save
|
||||
pm2 startup
|
||||
```
|
||||
|
||||
**方式 C:Windows 服务**
|
||||
|
||||
使用 [nssm](https://nssm.cc/) 将 `node server.js` 注册为 Windows 服务,开机自启。
|
||||
|
||||
### 3. 反向代理(可选)
|
||||
|
||||
若需通过 80 端口或 HTTPS 访问,可在 Nginx 中配置:
|
||||
|
||||
```nginx
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:8080;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 防火墙
|
||||
|
||||
在内网服务器开放 `project.config.json` 中配置的端口(默认 8080)。
|
||||
|
||||
## API
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| GET | `/` | 在线编辑器 |
|
||||
| GET | `/api/health` | 健康检查 |
|
||||
| POST | `/api/export` | 提交 JSON 配置,返回 HTML 文件下载 |
|
||||
| POST | `/api/import` | 提交 HTML 文本,解析其中的配置 JSON |
|
||||
|
||||
## 文件说明
|
||||
|
||||
| 文件 | 说明 |
|
||||
|------|------|
|
||||
| `server.js` | Web 服务主程序 |
|
||||
| `飞书审批配置文件_V1.0.html` | 编辑器模板(服务与导出共用) |
|
||||
| `project.config.json` | 端口、服务名称等配置 |
|
||||
| `deploy/start.sh` | CentOS 7 启动脚本 |
|
||||
| `deploy/feishu-approval.service` | systemd 服务单元示例 |
|
||||
|
||||
## 使用流程
|
||||
|
||||
1. 访问服务地址,开始新建或导入已有 HTML
|
||||
2. 完成表单与流程设计
|
||||
3. 点击 **「保存并下载 HTML」**,获得独立配置文件
|
||||
4. 下载的 HTML 可双击离线打开;再次编辑可导入回在线服务
|
||||
|
||||
## 健康检查
|
||||
|
||||
```bash
|
||||
curl http://localhost:8080/api/health
|
||||
```
|
||||
292
address-regions.js
Normal file
292
address-regions.js
Normal file
File diff suppressed because one or more lines are too long
17
deploy/feishu-approval.service
Normal file
17
deploy/feishu-approval.service
Normal file
@@ -0,0 +1,17 @@
|
||||
[Unit]
|
||||
Description=Feishu Approval Designer Service
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
WorkingDirectory=/opt/feishu-approval
|
||||
Environment=NODE_ENV=production
|
||||
Environment=PORT=8080
|
||||
Environment=HOST=0.0.0.0
|
||||
ExecStart=/usr/bin/node server.js
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
18
deploy/start.sh
Normal file
18
deploy/start.sh
Normal file
@@ -0,0 +1,18 @@
|
||||
#!/bin/bash
|
||||
# CentOS 7 启动脚本(Node.js 16.x)
|
||||
set -e
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
if ! command -v node >/dev/null 2>&1; then
|
||||
echo "[ERROR] node not found. Install Node.js 16.x first."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
NODE_MAJOR=$(node -p "process.versions.node.split('.')[0]")
|
||||
if [ "$NODE_MAJOR" -lt 16 ]; then
|
||||
echo "[ERROR] Node.js 16+ required. Current: $(node -v)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Starting with Node $(node -v) ..."
|
||||
exec node server.js
|
||||
14
package.json
Normal file
14
package.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "feishu-approval-designer",
|
||||
"private": true,
|
||||
"version": "2.0.0",
|
||||
"description": "飞书审批配置在线设计服务",
|
||||
"scripts": {
|
||||
"start": "node server.js",
|
||||
"dev": "node server.js",
|
||||
"build:regions": "node scripts/build-address-regions.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
}
|
||||
}
|
||||
6
project.config.json
Normal file
6
project.config.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"projectId": "approval_of_design",
|
||||
"port": 8080,
|
||||
"host": "0.0.0.0",
|
||||
"label": "飞书审批配置服务"
|
||||
}
|
||||
424
scripts/build-address-regions.js
Normal file
424
scripts/build-address-regions.js
Normal file
@@ -0,0 +1,424 @@
|
||||
/* eslint-disable */
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
function mkTree(stateMap) {
|
||||
const t = {};
|
||||
Object.keys(stateMap).forEach(function (st) {
|
||||
t[st] = {};
|
||||
stateMap[st].forEach(function (c) { t[st][c] = ['—']; });
|
||||
});
|
||||
return t;
|
||||
}
|
||||
|
||||
function mkFixed(fixedRegion, cities) {
|
||||
const t = {};
|
||||
t[fixedRegion] = {};
|
||||
Object.keys(cities).forEach(function (city) {
|
||||
t[fixedRegion][city] = cities[city];
|
||||
});
|
||||
return t;
|
||||
}
|
||||
|
||||
function mkFlat(cities) {
|
||||
const t = {};
|
||||
cities.forEach(function (c) { t[c] = ['—']; });
|
||||
return t;
|
||||
}
|
||||
|
||||
const US_STATES = {
|
||||
Alabama: ['Birmingham', 'Montgomery', 'Mobile', 'Huntsville', 'Tuscaloosa'],
|
||||
Alaska: ['Anchorage', 'Fairbanks', 'Juneau', 'Sitka', 'Ketchikan'],
|
||||
Arizona: ['Phoenix', 'Tucson', 'Mesa', 'Chandler', 'Scottsdale'],
|
||||
Arkansas: ['Little Rock', 'Fort Smith', 'Fayetteville', 'Springdale', 'Jonesboro'],
|
||||
California: ['Los Angeles', 'San Francisco', 'San Diego', 'San Jose', 'Sacramento', 'Oakland', 'Fresno', 'Long Beach'],
|
||||
Colorado: ['Denver', 'Colorado Springs', 'Aurora', 'Fort Collins', 'Lakewood'],
|
||||
Connecticut: ['Bridgeport', 'New Haven', 'Hartford', 'Stamford', 'Waterbury'],
|
||||
Delaware: ['Wilmington', 'Dover', 'Newark', 'Middletown', 'Smyrna'],
|
||||
'District of Columbia': ['Washington'],
|
||||
Florida: ['Jacksonville', 'Miami', 'Tampa', 'Orlando', 'St. Petersburg', 'Fort Lauderdale'],
|
||||
Georgia: ['Atlanta', 'Augusta', 'Columbus', 'Savannah', 'Athens'],
|
||||
Hawaii: ['Honolulu', 'Pearl City', 'Hilo', 'Kailua', 'Waipahu'],
|
||||
Idaho: ['Boise', 'Meridian', 'Nampa', 'Idaho Falls', 'Pocatello'],
|
||||
Illinois: ['Chicago', 'Aurora', 'Naperville', 'Joliet', 'Rockford', 'Springfield'],
|
||||
Indiana: ['Indianapolis', 'Fort Wayne', 'Evansville', 'South Bend', 'Carmel'],
|
||||
Iowa: ['Des Moines', 'Cedar Rapids', 'Davenport', 'Sioux City', 'Iowa City'],
|
||||
Kansas: ['Wichita', 'Overland Park', 'Kansas City', 'Topeka', 'Olathe'],
|
||||
Kentucky: ['Louisville', 'Lexington', 'Bowling Green', 'Owensboro', 'Covington'],
|
||||
Louisiana: ['New Orleans', 'Baton Rouge', 'Shreveport', 'Lafayette', 'Lake Charles'],
|
||||
Maine: ['Portland', 'Lewiston', 'Bangor', 'South Portland', 'Auburn'],
|
||||
Maryland: ['Baltimore', 'Frederick', 'Rockville', 'Gaithersburg', 'Bowie'],
|
||||
Massachusetts: ['Boston', 'Worcester', 'Springfield', 'Cambridge', 'Lowell'],
|
||||
Michigan: ['Detroit', 'Grand Rapids', 'Warren', 'Sterling Heights', 'Ann Arbor', 'Lansing'],
|
||||
Minnesota: ['Minneapolis', 'Saint Paul', 'Rochester', 'Duluth', 'Bloomington'],
|
||||
Mississippi: ['Jackson', 'Gulfport', 'Southaven', 'Hattiesburg', 'Biloxi'],
|
||||
Missouri: ['Kansas City', 'Saint Louis', 'Springfield', 'Columbia', 'Independence'],
|
||||
Montana: ['Billings', 'Missoula', 'Great Falls', 'Bozeman', 'Butte'],
|
||||
Nebraska: ['Omaha', 'Lincoln', 'Bellevue', 'Grand Island', 'Kearney'],
|
||||
Nevada: ['Las Vegas', 'Henderson', 'Reno', 'North Las Vegas', 'Sparks'],
|
||||
'New Hampshire': ['Manchester', 'Nashua', 'Concord', 'Dover', 'Rochester'],
|
||||
'New Jersey': ['Newark', 'Jersey City', 'Paterson', 'Elizabeth', 'Edison', 'Trenton'],
|
||||
'New Mexico': ['Albuquerque', 'Las Cruces', 'Rio Rancho', 'Santa Fe', 'Roswell'],
|
||||
'New York': ['New York City', 'Buffalo', 'Rochester', 'Yonkers', 'Syracuse', 'Albany'],
|
||||
'North Carolina': ['Charlotte', 'Raleigh', 'Greensboro', 'Durham', 'Winston-Salem'],
|
||||
'North Dakota': ['Fargo', 'Bismarck', 'Grand Forks', 'Minot', 'West Fargo'],
|
||||
Ohio: ['Columbus', 'Cleveland', 'Cincinnati', 'Toledo', 'Akron', 'Dayton'],
|
||||
Oklahoma: ['Oklahoma City', 'Tulsa', 'Norman', 'Broken Arrow', 'Lawton'],
|
||||
Oregon: ['Portland', 'Eugene', 'Salem', 'Gresham', 'Hillsboro'],
|
||||
Pennsylvania: ['Philadelphia', 'Pittsburgh', 'Allentown', 'Erie', 'Reading', 'Harrisburg'],
|
||||
'Rhode Island': ['Providence', 'Warwick', 'Cranston', 'Pawtucket', 'East Providence'],
|
||||
'South Carolina': ['Charleston', 'Columbia', 'North Charleston', 'Mount Pleasant', 'Greenville'],
|
||||
'South Dakota': ['Sioux Falls', 'Rapid City', 'Aberdeen', 'Brookings', 'Watertown'],
|
||||
Tennessee: ['Nashville', 'Memphis', 'Knoxville', 'Chattanooga', 'Clarksville'],
|
||||
Texas: ['Houston', 'San Antonio', 'Dallas', 'Austin', 'Fort Worth', 'El Paso', 'Arlington', 'Corpus Christi'],
|
||||
Utah: ['Salt Lake City', 'West Valley City', 'Provo', 'West Jordan', 'Orem'],
|
||||
Vermont: ['Burlington', 'South Burlington', 'Rutland', 'Barre', 'Montpelier'],
|
||||
Virginia: ['Virginia Beach', 'Norfolk', 'Chesapeake', 'Richmond', 'Newport News', 'Alexandria'],
|
||||
Washington: ['Seattle', 'Spokane', 'Tacoma', 'Vancouver', 'Bellevue', 'Olympia'],
|
||||
'West Virginia': ['Charleston', 'Huntington', 'Morgantown', 'Parkersburg', 'Wheeling'],
|
||||
Wisconsin: ['Milwaukee', 'Madison', 'Green Bay', 'Kenosha', 'Racine'],
|
||||
Wyoming: ['Cheyenne', 'Casper', 'Laramie', 'Gillette', 'Rock Springs']
|
||||
};
|
||||
|
||||
const CHINA = require('./china-regions-data.json');
|
||||
|
||||
const HK = mkFixed('香港特别行政区', {
|
||||
'香港岛': ['中西区', '湾仔区', '东区', '南区'],
|
||||
'九龙': ['油尖旺区', '深水埗区', '九龙城区', '黄大仙区', '观塘区'],
|
||||
'新界': ['荃湾区', '屯门区', '元朗区', '北区', '大埔区', '沙田区', '西贡区', '葵青区', '离岛区']
|
||||
});
|
||||
|
||||
const MO = mkFixed('澳门特别行政区', {
|
||||
'花地玛堂区': ['—'], '花王堂区': ['—'], '望德堂区': ['—'], '大堂区': ['—'],
|
||||
'风顺堂区': ['—'], '嘉模堂区': ['—'], '圣方济各堂区': ['—'], '路氹填海区': ['—']
|
||||
});
|
||||
|
||||
const TW = mkFixed('台湾省', {
|
||||
'台北市': ['中正区', '大同区', '中山区', '松山区', '大安区', '万华区', '信义区', '士林区', '北投区', '内湖区', '南港区', '文山区'],
|
||||
'新北市': ['板桥区', '三重区', '中和区', '永和区', '新庄区', '新店区', '树林区', '莺歌区', '三峡区', '淡水区', '汐止区', '瑞芳区', '土城区', '芦洲区', '五股区', '泰山区', '林口区'],
|
||||
'桃园市': ['桃园区', '中坜区', '平镇区', '八德区', '杨梅区', '芦竹区', '大溪区', '龙潭区', '龟山区', '大园区', '观音区', '新屋区', '复兴区'],
|
||||
'台中市': ['中区', '东区', '南区', '西区', '北区', '北屯区', '西屯区', '南屯区', '太平区', '大里区', '雾峰区', '乌日区', '丰原区', '后里区', '石冈区', '东势区', '和平区', '新社区', '潭子区', '大雅区', '神冈区', '大肚区', '沙鹿区', '龙井区', '梧栖区', '清水区', '大甲区', '外埔区', '大安区'],
|
||||
'台南市': ['中西区', '东区', '南区', '北区', '安平区', '安南区', '永康区', '归仁区', '新化区', '左镇区', '玉井区', '楠西区', '南化区', '仁德区', '关庙区', '龙崎区', '官田区', '麻豆区', '佳里区', '西港区', '七股区', '将军区', '学甲区', '北门区', '新营区', '后壁区', '白河区', '东山区', '六甲区', '下营区', '柳营区', '盐水区', '大内区', '山上区', '新市区', '安定区'],
|
||||
'高雄市': ['新兴区', '前金区', '苓雅区', '盐埕区', '鼓山区', '旗津区', '前镇区', '三民区', '楠梓区', '小港区', '左营区', '仁武区', '大社区', '冈山区', '路竹区', '阿莲区', '田寮区', '燕巢区', '桥头区', '梓官区', '弥陀区', '永安区', '湖内区', '凤山区', '大寮区', '林园区', '鸟松区', '大树区', '旗山区', '美浓区', '六龟区', '内门区', '杉林区', '甲仙区', '桃源区', '那玛夏区', '茂林区', '茄萣区'],
|
||||
'基隆市': ['仁爱区', '信义区', '中正区', '中山区', '安乐区', '暖暖区', '七堵区'],
|
||||
'新竹市': ['东区', '北区', '香山区'],
|
||||
'嘉义市': ['东区', '西区'],
|
||||
'新竹县': ['竹北市', '湖口乡', '新丰乡', '新埔镇', '关西镇', '芎林乡', '宝山乡', '竹东镇', '五峰乡', '横山乡', '尖石乡', '北埔乡', '峨眉乡'],
|
||||
'苗栗县': ['苗栗市', '苑里镇', '通霄镇', '竹南镇', '头份市', '后龙镇', '卓兰镇', '大湖乡', '公馆乡', '铜锣乡', '南庄乡', '头屋乡', '三义乡', '西湖乡', '造桥乡', '三湾乡', '狮潭乡', '泰安乡'],
|
||||
'彰化县': ['彰化市', '鹿港镇', '和美镇', '线西乡', '伸港乡', '福兴乡', '秀水乡', '花坛乡', '芬园乡', '员林镇', '溪湖镇', '田中镇', '大村乡', '埔盐乡', '埔心乡', '永靖乡', '社头乡', '二水乡', '北斗镇', '埤头乡', '溪州乡', '竹塘乡', '大城乡', '芳苑乡', '二林镇'],
|
||||
'南投县': ['南投市', '埔里镇', '草屯镇', '竹山镇', '集集镇', '名间乡', '鹿谷乡', '中寮乡', '鱼池乡', '国姓乡', '水里乡', '信义乡', '仁爱乡'],
|
||||
'云林县': ['斗六市', '斗南镇', '虎尾镇', '西螺镇', '土库镇', '北港镇', '古坑乡', '大埤乡', '莿桐乡', '林内乡', '二仑乡', '仑背乡', '麦寮乡', '东势乡', '褒忠乡', '台西乡', '元长乡', '四湖乡', '口湖乡', '水林乡'],
|
||||
'嘉义县': ['太保市', '朴子市', '布袋镇', '大林镇', '民雄乡', '溪口乡', '新港乡', '六脚乡', '东石乡', '义竹乡', '鹿草乡', '水上乡', '中埔乡', '竹崎乡', '梅山乡', '番路乡', '大埔乡', '阿里山乡'],
|
||||
'屏东县': ['屏东市', '潮州镇', '东港镇', '恒春镇', '万丹乡', '长治乡', '麟洛乡', '九如乡', '里港乡', '盐埔乡', '高树乡', '万峦乡', '内埔乡', '竹田乡', '新埤乡', '枋寮乡', '新园乡', '崁顶乡', '林边乡', '南州乡', '佳冬乡', '琉球乡', '车城乡', '满州乡', '枋山乡', '三地门乡', '雾台乡', '玛家乡', '泰武乡', '来义乡', '春日乡', '狮子乡', '牡丹乡'],
|
||||
'宜兰县': ['宜兰市', '罗东镇', '苏澳镇', '头城镇', '礁溪乡', '壮围乡', '员山乡', '冬山乡', '五结乡', '三星乡', '大同乡', '南澳乡'],
|
||||
'花莲县': ['花莲市', '凤林镇', '玉里镇', '新城乡', '吉安乡', '寿丰乡', '光复乡', '丰滨乡', '瑞穗乡', '富里乡', '秀林乡', '万荣乡', '卓溪乡'],
|
||||
'台东县': ['台东市', '成功镇', '关山镇', '卑南乡', '鹿野乡', '池上乡', '东河乡', '长滨乡', '太麻里乡', '大武乡', '绿岛乡', '海端乡', '延平乡', '金峰乡', '达仁乡', '兰屿乡'],
|
||||
'澎湖县': ['马公市', '湖西乡', '白沙乡', '西屿乡', '望安乡', '七美乡'],
|
||||
'金门县': ['金城镇', '金湖镇', '金沙镇', '金宁乡', '烈屿乡', '乌坵乡'],
|
||||
'连江县': ['南竿乡', '北竿乡', '莒光乡', '东引乡']
|
||||
});
|
||||
|
||||
const JP = mkTree({
|
||||
Hokkaido: ['Sapporo', 'Asahikawa', 'Hakodate', 'Kushiro', 'Obihiro'],
|
||||
Aomori: ['Aomori', 'Hachinohe', 'Hirosaki'],
|
||||
Iwate: ['Morioka', 'Ofunato', 'Hanamaki'],
|
||||
Miyagi: ['Sendai', 'Ishinomaki', 'Osaki'],
|
||||
Akita: ['Akita', 'Yokote', 'Oga'],
|
||||
Yamagata: ['Yamagata', 'Tsuruoka', 'Sakata'],
|
||||
Fukushima: ['Fukushima', 'Koriyama', 'Iwaki'],
|
||||
Ibaraki: ['Mito', 'Tsukuba', 'Hitachi'],
|
||||
Tochigi: ['Utsunomiya', 'Ashikaga', 'Oyama'],
|
||||
Gunma: ['Maebashi', 'Takasaki', 'Ota'],
|
||||
Saitama: ['Saitama', 'Kawagoe', 'Kawaguchi'],
|
||||
Chiba: ['Chiba', 'Funabashi', 'Ichikawa'],
|
||||
Tokyo: ['Shinjuku', 'Shibuya', 'Minato', 'Chiyoda', 'Setagaya'],
|
||||
Kanagawa: ['Yokohama', 'Kawasaki', 'Sagamihara'],
|
||||
Niigata: ['Niigata', 'Nagaoka', 'Joetsu'],
|
||||
Toyama: ['Toyama', 'Takaoka', 'Uozu'],
|
||||
Ishikawa: ['Kanazawa', 'Komatsu', 'Hakusan'],
|
||||
Fukui: ['Fukui', 'Sakai', 'Obama'],
|
||||
Yamanashi: ['Kofu', 'Fujiyoshida', 'Kai'],
|
||||
Nagano: ['Nagano', 'Matsumoto', 'Ueda'],
|
||||
Gifu: ['Gifu', 'Ogaki', 'Takayama'],
|
||||
Shizuoka: ['Shizuoka', 'Hamamatsu', 'Numazu'],
|
||||
Aichi: ['Nagoya', 'Toyohashi', 'Okazaki'],
|
||||
Mie: ['Tsu', 'Yokkaichi', 'Suzuka'],
|
||||
Shiga: ['Otsu', 'Hikone', 'Kusatsu'],
|
||||
Kyoto: ['Kyoto', 'Uji', 'Maizuru'],
|
||||
Osaka: ['Osaka', 'Sakai', 'Higashiosaka'],
|
||||
Hyogo: ['Kobe', 'Himeji', 'Nishinomiya'],
|
||||
Nara: ['Nara', 'Kashihara', 'Ikoma'],
|
||||
Wakayama: ['Wakayama', 'Tanabe', 'Kinokawa'],
|
||||
Tottori: ['Tottori', 'Yonago', 'Kurayoshi'],
|
||||
Shimane: ['Matsue', 'Izumo', 'Hamada'],
|
||||
Okayama: ['Okayama', 'Kurashiki', 'Tsuyama'],
|
||||
Hiroshima: ['Hiroshima', 'Fukuyama', 'Kure'],
|
||||
Yamaguchi: ['Yamaguchi', 'Shimonoseki', 'Ube'],
|
||||
Tokushima: ['Tokushima', 'Naruto', 'Anan'],
|
||||
Kagawa: ['Takamatsu', 'Marugame', 'Sakaide'],
|
||||
Ehime: ['Matsuyama', 'Imabari', 'Uwajima'],
|
||||
Kochi: ['Kochi', 'Nankoku', 'Shimanto'],
|
||||
Fukuoka: ['Fukuoka', 'Kitakyushu', 'Kurume'],
|
||||
Saga: ['Saga', 'Karatsu', 'Tosu'],
|
||||
Nagasaki: ['Nagasaki', 'Sasebo', 'Isahaya'],
|
||||
Kumamoto: ['Kumamoto', 'Yatsushiro', 'Amakusa'],
|
||||
Oita: ['Oita', 'Beppu', 'Nakatsu'],
|
||||
Miyazaki: ['Miyazaki', 'Miyakonojo', 'Nobeoka'],
|
||||
Kagoshima: ['Kagoshima', 'Kanoya', 'Satsumasendai'],
|
||||
Okinawa: ['Naha', 'Okinawa', 'Uruma']
|
||||
});
|
||||
|
||||
const KR = mkTree({
|
||||
Seoul: ['Jongno-gu', 'Jung-gu', 'Gangnam-gu', 'Mapo-gu', 'Seocho-gu'],
|
||||
Busan: ['Jung-gu', 'Seo-gu', 'Haeundae-gu', 'Saha-gu'],
|
||||
Daegu: ['Jung-gu', 'Dong-gu', 'Seo-gu'],
|
||||
Incheon: ['Jung-gu', 'Nam-gu', 'Yeonsu-gu'],
|
||||
Gwangju: ['Dong-gu', 'Seo-gu', 'Nam-gu'],
|
||||
Daejeon: ['Jung-gu', 'Dong-gu', 'Seo-gu'],
|
||||
Ulsan: ['Jung-gu', 'Nam-gu', 'Dong-gu'],
|
||||
Sejong: ['Sejong'],
|
||||
Gyeonggi: ['Suwon', 'Seongnam', 'Goyang', 'Yongin', 'Bucheon'],
|
||||
Gangwon: ['Chuncheon', 'Wonju', 'Gangneung'],
|
||||
'North Chungcheong': ['Cheongju', 'Chungju', 'Jecheon'],
|
||||
'South Chungcheong': ['Daejeon', 'Cheonan', 'Asan'],
|
||||
'North Jeolla': ['Jeonju', 'Iksan', 'Gunsan'],
|
||||
'South Jeolla': ['Gwangju', 'Mokpo', 'Suncheon'],
|
||||
'North Gyeongsang': ['Pohang', 'Gyeongju', 'Gimhae'],
|
||||
'South Gyeongsang': ['Changwon', 'Jinju', 'Geoje'],
|
||||
Jeju: ['Jeju', 'Seogwipo']
|
||||
});
|
||||
|
||||
const CA = mkTree({
|
||||
Alberta: ['Calgary', 'Edmonton', 'Red Deer', 'Lethbridge'],
|
||||
'British Columbia': ['Vancouver', 'Victoria', 'Surrey', 'Burnaby', 'Kelowna'],
|
||||
Manitoba: ['Winnipeg', 'Brandon', 'Steinbach'],
|
||||
'New Brunswick': ['Moncton', 'Saint John', 'Fredericton'],
|
||||
'Newfoundland and Labrador': ["St. John's", 'Corner Brook', 'Mount Pearl'],
|
||||
'Nova Scotia': ['Halifax', 'Dartmouth', 'Sydney'],
|
||||
Ontario: ['Toronto', 'Ottawa', 'Mississauga', 'Hamilton', 'London', 'Kitchener'],
|
||||
'Prince Edward Island': ['Charlottetown', 'Summerside'],
|
||||
Quebec: ['Montreal', 'Quebec City', 'Laval', 'Gatineau', 'Sherbrooke'],
|
||||
Saskatchewan: ['Saskatoon', 'Regina', 'Prince Albert'],
|
||||
Yukon: ['Whitehorse', 'Dawson City'],
|
||||
'Northwest Territories': ['Yellowknife', 'Hay River'],
|
||||
Nunavut: ['Iqaluit', 'Rankin Inlet']
|
||||
});
|
||||
|
||||
const AU = mkTree({
|
||||
'New South Wales': ['Sydney', 'Newcastle', 'Wollongong', 'Central Coast'],
|
||||
Victoria: ['Melbourne', 'Geelong', 'Ballarat', 'Bendigo'],
|
||||
Queensland: ['Brisbane', 'Gold Coast', 'Sunshine Coast', 'Townsville', 'Cairns'],
|
||||
'Western Australia': ['Perth', 'Fremantle', 'Mandurah', 'Bunbury'],
|
||||
'South Australia': ['Adelaide', 'Mount Gambier', 'Whyalla'],
|
||||
Tasmania: ['Hobart', 'Launceston', 'Devonport'],
|
||||
'Australian Capital Territory': ['Canberra'],
|
||||
'Northern Territory': ['Darwin', 'Alice Springs', 'Palmerston']
|
||||
});
|
||||
|
||||
const GB = mkTree({
|
||||
England: ['London', 'Manchester', 'Birmingham', 'Leeds', 'Liverpool', 'Bristol', 'Sheffield'],
|
||||
Scotland: ['Edinburgh', 'Glasgow', 'Aberdeen', 'Dundee'],
|
||||
Wales: ['Cardiff', 'Swansea', 'Newport', 'Wrexham'],
|
||||
'Northern Ireland': ['Belfast', 'Derry', 'Lisburn', 'Newry']
|
||||
});
|
||||
|
||||
const DE = mkTree({
|
||||
'Baden-Wurttemberg': ['Stuttgart', 'Mannheim', 'Karlsruhe', 'Freiburg'],
|
||||
Bavaria: ['Munich', 'Nuremberg', 'Augsburg', 'Regensburg'],
|
||||
Berlin: ['Berlin'],
|
||||
Brandenburg: ['Potsdam', 'Cottbus', 'Frankfurt (Oder)'],
|
||||
Bremen: ['Bremen', 'Bremerhaven'],
|
||||
Hamburg: ['Hamburg'],
|
||||
Hesse: ['Frankfurt am Main', 'Wiesbaden', 'Darmstadt', 'Kassel'],
|
||||
'Lower Saxony': ['Hanover', 'Braunschweig', 'Osnabruck', 'Oldenburg'],
|
||||
'Mecklenburg-Vorpommern': ['Rostock', 'Schwerin', 'Stralsund'],
|
||||
'North Rhine-Westphalia': ['Cologne', 'Dusseldorf', 'Dortmund', 'Essen', 'Bonn'],
|
||||
'Rhineland-Palatinate': ['Mainz', 'Ludwigshafen', 'Koblenz', 'Trier'],
|
||||
Saarland: ['Saarbrucken', 'Neunkirchen', 'Homburg'],
|
||||
Saxony: ['Dresden', 'Leipzig', 'Chemnitz'],
|
||||
'Saxony-Anhalt': ['Magdeburg', 'Halle (Saale)', 'Dessau-Rosslau'],
|
||||
'Schleswig-Holstein': ['Kiel', 'Lubeck', 'Flensburg'],
|
||||
Thuringia: ['Erfurt', 'Jena', 'Gera', 'Weimar']
|
||||
});
|
||||
|
||||
const FR = mkTree({
|
||||
'Auvergne-Rhone-Alpes': ['Lyon', 'Grenoble', 'Saint-Etienne', 'Clermont-Ferrand'],
|
||||
'Bourgogne-Franche-Comte': ['Dijon', 'Besancon'],
|
||||
Brittany: ['Rennes', 'Brest', 'Nantes'],
|
||||
'Centre-Val de Loire': ['Orleans', 'Tours', 'Bourges'],
|
||||
Corsica: ['Ajaccio', 'Bastia'],
|
||||
'Grand Est': ['Strasbourg', 'Reims', 'Metz', 'Nancy'],
|
||||
'Hauts-de-France': ['Lille', 'Amiens', 'Roubaix'],
|
||||
'Ile-de-France': ['Paris', 'Versailles', 'Boulogne-Billancourt', 'Saint-Denis'],
|
||||
Normandy: ['Rouen', 'Le Havre', 'Caen'],
|
||||
'Nouvelle-Aquitaine': ['Bordeaux', 'Limoges', 'Poitiers'],
|
||||
Occitanie: ['Toulouse', 'Montpellier', 'Nimes'],
|
||||
'Pays de la Loire': ['Nantes', 'Angers', 'Le Mans'],
|
||||
'Provence-Alpes-Cote dAzur': ['Marseille', 'Nice', 'Toulon', 'Aix-en-Provence']
|
||||
});
|
||||
|
||||
const COUNTRY_META = {
|
||||
'中国': { lang: 'zh', hasRegion: true, hasDistrict: true },
|
||||
'中国香港': { lang: 'zh', fixedRegion: '香港特别行政区', hasRegion: true, hasDistrict: true },
|
||||
'中国澳门': { lang: 'zh', fixedRegion: '澳门特别行政区', hasRegion: true, hasDistrict: false },
|
||||
'中国台湾': { lang: 'zh', fixedRegion: '台湾省', hasRegion: true, hasDistrict: true },
|
||||
'美国': { lang: 'en', hasRegion: true, hasDistrict: false },
|
||||
'日本': { lang: 'en', hasRegion: true, hasDistrict: false },
|
||||
'韩国': { lang: 'en', hasRegion: true, hasDistrict: false },
|
||||
'新加坡': { lang: 'en', hasRegion: false, hasDistrict: false },
|
||||
'马来西亚': { lang: 'en', hasRegion: true, hasDistrict: false },
|
||||
'泰国': { lang: 'en', hasRegion: true, hasDistrict: false },
|
||||
'越南': { lang: 'en', hasRegion: true, hasDistrict: false },
|
||||
'印度': { lang: 'en', hasRegion: true, hasDistrict: false },
|
||||
'印度尼西亚': { lang: 'en', hasRegion: true, hasDistrict: false },
|
||||
'菲律宾': { lang: 'en', hasRegion: true, hasDistrict: false },
|
||||
'英国': { lang: 'en', hasRegion: true, hasDistrict: false },
|
||||
'法国': { lang: 'en', hasRegion: true, hasDistrict: false },
|
||||
'德国': { lang: 'en', hasRegion: true, hasDistrict: false },
|
||||
'意大利': { lang: 'en', hasRegion: true, hasDistrict: false },
|
||||
'西班牙': { lang: 'en', hasRegion: true, hasDistrict: false },
|
||||
'荷兰': { lang: 'en', hasRegion: true, hasDistrict: false },
|
||||
'瑞士': { lang: 'en', hasRegion: true, hasDistrict: false },
|
||||
'瑞典': { lang: 'en', hasRegion: true, hasDistrict: false },
|
||||
'挪威': { lang: 'en', hasRegion: true, hasDistrict: false },
|
||||
'丹麦': { lang: 'en', hasRegion: true, hasDistrict: false },
|
||||
'芬兰': { lang: 'en', hasRegion: true, hasDistrict: false },
|
||||
'俄罗斯': { lang: 'en', hasRegion: true, hasDistrict: false },
|
||||
'波兰': { lang: 'en', hasRegion: true, hasDistrict: false },
|
||||
'捷克': { lang: 'en', hasRegion: true, hasDistrict: false },
|
||||
'奥地利': { lang: 'en', hasRegion: true, hasDistrict: false },
|
||||
'比利时': { lang: 'en', hasRegion: true, hasDistrict: false },
|
||||
'加拿大': { lang: 'en', hasRegion: true, hasDistrict: false },
|
||||
'墨西哥': { lang: 'en', hasRegion: true, hasDistrict: false },
|
||||
'巴西': { lang: 'en', hasRegion: true, hasDistrict: false },
|
||||
'阿根廷': { lang: 'en', hasRegion: true, hasDistrict: false },
|
||||
'智利': { lang: 'en', hasRegion: true, hasDistrict: false },
|
||||
'哥伦比亚': { lang: 'en', hasRegion: true, hasDistrict: false },
|
||||
'澳大利亚': { lang: 'en', hasRegion: true, hasDistrict: false },
|
||||
'新西兰': { lang: 'en', hasRegion: true, hasDistrict: false },
|
||||
'南非': { lang: 'en', hasRegion: true, hasDistrict: false },
|
||||
'埃及': { lang: 'en', hasRegion: true, hasDistrict: false },
|
||||
'阿联酋': { lang: 'en', hasRegion: true, hasDistrict: false },
|
||||
'沙特阿拉伯': { lang: 'en', hasRegion: true, hasDistrict: false },
|
||||
'以色列': { lang: 'en', hasRegion: true, hasDistrict: false },
|
||||
'土耳其': { lang: 'en', hasRegion: true, hasDistrict: false },
|
||||
'其他': { lang: 'en', hasRegion: true, hasDistrict: false }
|
||||
};
|
||||
|
||||
const WORLD_REGIONS = {
|
||||
'中国': CHINA,
|
||||
'中国香港': HK,
|
||||
'中国澳门': MO,
|
||||
'中国台湾': TW,
|
||||
'美国': mkTree(US_STATES),
|
||||
'日本': JP,
|
||||
'韩国': KR,
|
||||
'新加坡': mkFlat(['Central Region', 'East Region', 'North Region', 'North-East Region', 'West Region']),
|
||||
'马来西亚': mkTree({ 'Kuala Lumpur': ['Kuala Lumpur'], Selangor: ['Shah Alam', 'Petaling Jaya'], Penang: ['George Town'], Johor: ['Johor Bahru'] }),
|
||||
'泰国': mkTree({ Bangkok: ['Bangkok'], ChiangMai: ['Chiang Mai'], Phuket: ['Phuket'] }),
|
||||
'越南': mkTree({ Hanoi: ['Hanoi'], 'Ho Chi Minh City': ['Ho Chi Minh City'], DaNang: ['Da Nang'] }),
|
||||
'印度': mkTree({ Delhi: ['New Delhi'], Maharashtra: ['Mumbai', 'Pune'], Karnataka: ['Bengaluru'] }),
|
||||
'印度尼西亚': mkTree({ Jakarta: ['Central Jakarta', 'South Jakarta'], 'West Java': ['Bandung'], Bali: ['Denpasar'] }),
|
||||
'菲律宾': mkTree({ 'Metro Manila': ['Manila', 'Quezon City'], Cebu: ['Cebu City'], Davao: ['Davao City'] }),
|
||||
'英国': GB,
|
||||
'法国': FR,
|
||||
'德国': DE,
|
||||
'意大利': mkTree({ Lazio: ['Rome'], Lombardy: ['Milan'], Veneto: ['Venice'], Tuscany: ['Florence'] }),
|
||||
'西班牙': mkTree({ Madrid: ['Madrid'], Catalonia: ['Barcelona'], Andalusia: ['Seville'] }),
|
||||
'荷兰': mkTree({ 'North Holland': ['Amsterdam', 'Haarlem'], 'South Holland': ['Rotterdam', 'The Hague'] }),
|
||||
'瑞士': mkTree({ Zurich: ['Zurich'], Bern: ['Bern'], Geneva: ['Geneva'] }),
|
||||
'瑞典': mkTree({ Stockholm: ['Stockholm'], VastraGotaland: ['Gothenburg'], Skane: ['Malmo'] }),
|
||||
'挪威': mkTree({ Oslo: ['Oslo'], Viken: ['Drammen'], Vestland: ['Bergen'] }),
|
||||
'丹麦': mkTree({ 'Capital Region': ['Copenhagen'], 'Central Denmark': ['Aarhus'], 'Southern Denmark': ['Odense'] }),
|
||||
'芬兰': mkTree({ Uusimaa: ['Helsinki'], Pirkanmaa: ['Tampere'], 'Southwest Finland': ['Turku'] }),
|
||||
'俄罗斯': mkTree({ Moscow: ['Moscow'], 'Saint Petersburg': ['Saint Petersburg'], 'Novosibirsk Oblast': ['Novosibirsk'] }),
|
||||
'波兰': mkTree({ Masovian: ['Warsaw'], 'Lesser Poland': ['Krakow'], Silesian: ['Katowice'] }),
|
||||
'捷克': mkTree({ Prague: ['Prague'], 'South Moravian': ['Brno'] }),
|
||||
'奥地利': mkTree({ Vienna: ['Vienna'], Tyrol: ['Innsbruck'], Styria: ['Graz'] }),
|
||||
'比利时': mkTree({ Brussels: ['Brussels'], Flanders: ['Antwerp', 'Ghent'], Wallonia: ['Liege', 'Charleroi'] }),
|
||||
'加拿大': CA,
|
||||
'墨西哥': mkTree({ 'Mexico City': ['Mexico City'], Jalisco: ['Guadalajara'], 'Nuevo Leon': ['Monterrey'] }),
|
||||
'巴西': mkTree({ 'Sao Paulo': ['Sao Paulo'], 'Rio de Janeiro': ['Rio de Janeiro'], 'Minas Gerais': ['Belo Horizonte'] }),
|
||||
'阿根廷': mkTree({ 'Buenos Aires': ['Buenos Aires'], Cordoba: ['Cordoba'], Mendoza: ['Mendoza'] }),
|
||||
'智利': mkTree({ 'Santiago Metropolitan': ['Santiago'], Valparaiso: ['Valparaiso'], Biobio: ['Concepcion'] }),
|
||||
'哥伦比亚': mkTree({ Bogota: ['Bogota'], Antioquia: ['Medellin'], Valle: ['Cali'] }),
|
||||
'澳大利亚': AU,
|
||||
'新西兰': mkTree({ Auckland: ['Auckland'], Wellington: ['Wellington'], Canterbury: ['Christchurch'] }),
|
||||
'南非': mkTree({ Gauteng: ['Johannesburg', 'Pretoria'], 'Western Cape': ['Cape Town'], 'KwaZulu-Natal': ['Durban'] }),
|
||||
'埃及': mkTree({ Cairo: ['Cairo'], Alexandria: ['Alexandria'], Giza: ['Giza'] }),
|
||||
'阿联酋': mkTree({ Dubai: ['Dubai'], 'Abu Dhabi': ['Abu Dhabi'], Sharjah: ['Sharjah'] }),
|
||||
'沙特阿拉伯': mkTree({ Riyadh: ['Riyadh'], Makkah: ['Jeddah'], 'Eastern Province': ['Dammam'] }),
|
||||
'以色列': mkTree({ TelAviv: ['Tel Aviv'], Jerusalem: ['Jerusalem'], Haifa: ['Haifa'] }),
|
||||
'土耳其': mkTree({ Istanbul: ['Istanbul'], Ankara: ['Ankara'], Izmir: ['Izmir'] }),
|
||||
'其他': mkTree({ 'Province/State': ['City'] })
|
||||
};
|
||||
|
||||
const ADDRESS_COUNTRIES = Object.keys(COUNTRY_META);
|
||||
|
||||
const out = `/* Auto-generated address region data for Feishu-style address conditions */
|
||||
(function (g) {
|
||||
var ADDRESS_COUNTRIES = ${JSON.stringify(ADDRESS_COUNTRIES)};
|
||||
var COUNTRY_META = ${JSON.stringify(COUNTRY_META, null, 2)};
|
||||
var WORLD_REGIONS = ${JSON.stringify(WORLD_REGIONS)};
|
||||
|
||||
function getCountryMeta(country) {
|
||||
return COUNTRY_META[country] || COUNTRY_META['其他'];
|
||||
}
|
||||
|
||||
function getCountryRegionTree(country) {
|
||||
return WORLD_REGIONS[country] || WORLD_REGIONS['其他'];
|
||||
}
|
||||
|
||||
function getProvincesForCountry(country) {
|
||||
var meta = getCountryMeta(country);
|
||||
if (meta.fixedRegion) return [meta.fixedRegion];
|
||||
if (!meta.hasRegion) return [];
|
||||
var tree = getCountryRegionTree(country);
|
||||
return Object.keys(tree);
|
||||
}
|
||||
|
||||
function getCitiesForCountry(country, province) {
|
||||
var meta = getCountryMeta(country);
|
||||
var tree = getCountryRegionTree(country);
|
||||
if (!meta.hasRegion) return Object.keys(tree);
|
||||
if (!province) return [];
|
||||
if (meta.fixedRegion && tree[meta.fixedRegion]) return Object.keys(tree[meta.fixedRegion]);
|
||||
return tree[province] ? Object.keys(tree[province]) : [];
|
||||
}
|
||||
|
||||
function getDistrictsForCountry(country, province, city) {
|
||||
var meta = getCountryMeta(country);
|
||||
if (!meta.hasDistrict) return [];
|
||||
var tree = getCountryRegionTree(country);
|
||||
var regionKey = meta.fixedRegion || province;
|
||||
if (!regionKey || !city) return [];
|
||||
if (meta.fixedRegion && tree[meta.fixedRegion] && tree[meta.fixedRegion][city]) return tree[meta.fixedRegion][city];
|
||||
if (tree[regionKey] && tree[regionKey][city]) return tree[regionKey][city];
|
||||
return [];
|
||||
}
|
||||
|
||||
function applyCountryAddressDefaults(country, addr) {
|
||||
var meta = getCountryMeta(country);
|
||||
addr.country = country;
|
||||
if (meta.fixedRegion) addr.province = meta.fixedRegion;
|
||||
else if (!meta.hasRegion) addr.province = '';
|
||||
else if (addr.province && !getProvincesForCountry(country).includes(addr.province)) addr.province = '';
|
||||
if (addr.city && !getCitiesForCountry(country, addr.province || meta.fixedRegion).includes(addr.city)) addr.city = '';
|
||||
if (addr.district && !getDistrictsForCountry(country, addr.province, addr.city).includes(addr.district)) addr.district = '';
|
||||
return addr;
|
||||
}
|
||||
|
||||
g.ADDRESS_COUNTRIES = ADDRESS_COUNTRIES;
|
||||
g.COUNTRY_META = COUNTRY_META;
|
||||
g.WORLD_REGIONS = WORLD_REGIONS;
|
||||
g.getCountryMeta = getCountryMeta;
|
||||
g.getCountryRegionTree = getCountryRegionTree;
|
||||
g.getProvincesForCountry = getProvincesForCountry;
|
||||
g.getCitiesForCountry = getCitiesForCountry;
|
||||
g.getDistrictsForCountry = getDistrictsForCountry;
|
||||
g.applyCountryAddressDefaults = applyCountryAddressDefaults;
|
||||
})(typeof window !== 'undefined' ? window : global);
|
||||
`;
|
||||
|
||||
fs.writeFileSync(path.join(__dirname, '..', 'address-regions.js'), out, 'utf8');
|
||||
console.log('Wrote address-regions.js', (out.length / 1024).toFixed(1), 'KB');
|
||||
1
scripts/china-regions-data.json
Normal file
1
scripts/china-regions-data.json
Normal file
File diff suppressed because one or more lines are too long
205
server.js
Normal file
205
server.js
Normal file
@@ -0,0 +1,205 @@
|
||||
const http = require('http');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const url = require('url');
|
||||
|
||||
const ROOT = __dirname;
|
||||
const CONFIG_PATH = path.join(ROOT, 'project.config.json');
|
||||
const TEMPLATE_HTML = path.join(ROOT, '飞书审批配置文件_V1.0.html');
|
||||
const ADDRESS_REGIONS_JS = path.join(ROOT, 'address-regions.js');
|
||||
|
||||
function loadProjectConfig() {
|
||||
try {
|
||||
if (fs.existsSync(CONFIG_PATH)) return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
|
||||
} catch (e) {}
|
||||
return { projectId: 'approval_of_design', port: 8080, host: '0.0.0.0', label: '飞书审批配置服务' };
|
||||
}
|
||||
|
||||
const PROJECT = loadProjectConfig();
|
||||
const PORT = Number(process.env.PORT) || PROJECT.port || 8080;
|
||||
const HOST = process.env.HOST || PROJECT.host || '0.0.0.0';
|
||||
|
||||
const NODE_MAJOR = parseInt(String(process.versions.node).split('.')[0], 10);
|
||||
if (NODE_MAJOR < 16) {
|
||||
console.error('[ERROR] Node.js 16.x or later is required. Current: ' + process.version);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
let templateCache = null;
|
||||
|
||||
function readTemplate() {
|
||||
if (!templateCache) {
|
||||
templateCache = fs.readFileSync(TEMPLATE_HTML, 'utf8');
|
||||
}
|
||||
return templateCache;
|
||||
}
|
||||
|
||||
function reloadTemplate() {
|
||||
templateCache = null;
|
||||
return readTemplate();
|
||||
}
|
||||
|
||||
function safeJsonForScript(value) {
|
||||
return JSON.stringify(value).replace(/<\//g, '\\u003c/');
|
||||
}
|
||||
|
||||
function renderBootScript(config, mode) {
|
||||
const project = {
|
||||
id: PROJECT.projectId || 'approval_of_design',
|
||||
label: PROJECT.label || '飞书审批配置服务'
|
||||
};
|
||||
return [
|
||||
'<script id="app-boot">',
|
||||
'window.APP_PROJECT = ' + safeJsonForScript(project) + ';',
|
||||
"window.APP_MODE = '" + mode + "';",
|
||||
'window.__INITIAL_CONFIG__ = ' + safeJsonForScript(config) + ';',
|
||||
'</script>'
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
function injectBoot(html, config, mode) {
|
||||
return html.replace(/<script id="app-boot">[\s\S]*?<\/script>/, renderBootScript(config, mode));
|
||||
}
|
||||
|
||||
function inlineAddressRegions(html) {
|
||||
if (!fs.existsSync(ADDRESS_REGIONS_JS)) return html;
|
||||
const js = fs.readFileSync(ADDRESS_REGIONS_JS, 'utf8');
|
||||
return html.replace(
|
||||
/<script src="address-regions\.js"><\/script>/,
|
||||
'<script>\n' + js + '\n</script>'
|
||||
);
|
||||
}
|
||||
|
||||
function prepareHtml(config, mode) {
|
||||
return inlineAddressRegions(injectBoot(readTemplate(), config, mode));
|
||||
}
|
||||
|
||||
function sanitizeFilename(name) {
|
||||
return String(name || '审批配置')
|
||||
.replace(/[<>:"/\\|?*\x00-\x1f]/g, '_')
|
||||
.replace(/\s+/g, '_')
|
||||
.slice(0, 80) || '审批配置';
|
||||
}
|
||||
|
||||
function sendJson(res, status, data) {
|
||||
res.writeHead(status, { 'Content-Type': 'application/json; charset=utf-8' });
|
||||
res.end(JSON.stringify(data));
|
||||
}
|
||||
|
||||
function readBody(req, limit) {
|
||||
const maxBytes = limit || 20 * 1024 * 1024;
|
||||
return new Promise(function (resolve, reject) {
|
||||
var body = '';
|
||||
var size = 0;
|
||||
req.on('data', function (chunk) {
|
||||
size += chunk.length;
|
||||
if (size > maxBytes) {
|
||||
reject(new Error('request body too large'));
|
||||
req.destroy();
|
||||
return;
|
||||
}
|
||||
body += chunk;
|
||||
});
|
||||
req.on('end', function () { resolve(body); });
|
||||
req.on('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
function parseInitialConfigFromHtml(html) {
|
||||
const match = html.match(/window\.__INITIAL_CONFIG__\s*=\s*([\s\S]*?);\s*(?:\n|<\/script>)/);
|
||||
if (!match) return null;
|
||||
try {
|
||||
return JSON.parse(match[1]);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const server = http.createServer(async (req, res) => {
|
||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
||||
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
||||
|
||||
if (req.method === 'OPTIONS') {
|
||||
res.writeHead(204);
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
|
||||
const parsed = url.parse(req.url, true);
|
||||
const pathname = parsed.pathname;
|
||||
|
||||
try {
|
||||
if (req.method === 'GET' && (pathname === '/' || pathname === '/editor')) {
|
||||
const initial = parsed.query.config ? JSON.parse(parsed.query.config) : null;
|
||||
const html = prepareHtml(initial, 'server');
|
||||
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store' });
|
||||
res.end(html);
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.method === 'GET' && pathname === '/address-regions.js') {
|
||||
if (!fs.existsSync(ADDRESS_REGIONS_JS)) {
|
||||
res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
|
||||
res.end('address-regions.js not found');
|
||||
return;
|
||||
}
|
||||
res.writeHead(200, { 'Content-Type': 'application/javascript; charset=utf-8', 'Cache-Control': 'no-store' });
|
||||
res.end(fs.readFileSync(ADDRESS_REGIONS_JS, 'utf8'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.method === 'GET' && pathname === '/api/health') {
|
||||
sendJson(res, 200, { ok: true, service: PROJECT.label, projectId: PROJECT.projectId });
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.method === 'POST' && pathname === '/api/export') {
|
||||
const body = await readBody(req);
|
||||
const config = JSON.parse(body);
|
||||
config.exportedAt = new Date().toISOString();
|
||||
config.projectId = PROJECT.projectId;
|
||||
const html = prepareHtml(config, 'standalone');
|
||||
const filename = sanitizeFilename(config.approvalName) + '.html';
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'text/html; charset=utf-8',
|
||||
'Content-Disposition': 'attachment; filename="' + encodeURIComponent(filename) + '"',
|
||||
'Cache-Control': 'no-store'
|
||||
});
|
||||
res.end(html);
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.method === 'POST' && pathname === '/api/import') {
|
||||
const body = await readBody(req);
|
||||
const payload = JSON.parse(body);
|
||||
let config = null;
|
||||
if (payload.config) {
|
||||
config = payload.config;
|
||||
} else if (payload.html) {
|
||||
config = parseInitialConfigFromHtml(payload.html);
|
||||
}
|
||||
if (!config) {
|
||||
sendJson(res, 400, { ok: false, error: 'invalid import payload' });
|
||||
return;
|
||||
}
|
||||
sendJson(res, 200, { ok: true, config });
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.method === 'POST' && pathname === '/api/reload-template') {
|
||||
reloadTemplate();
|
||||
sendJson(res, 200, { ok: true });
|
||||
return;
|
||||
}
|
||||
|
||||
res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
|
||||
res.end('Not Found');
|
||||
} catch (err) {
|
||||
sendJson(res, 500, { ok: false, error: err.message || 'server error' });
|
||||
}
|
||||
});
|
||||
|
||||
server.listen(PORT, HOST, () => {
|
||||
console.log('[' + (PROJECT.label || 'approval') + '] http://' + HOST + ':' + PORT);
|
||||
});
|
||||
4182
飞书审批配置文件_V1.0.html
Normal file
4182
飞书审批配置文件_V1.0.html
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user