This commit is contained in:
张成
2026-03-24 16:07:02 +08:00
commit aa8eaa6ccd
121 changed files with 34042 additions and 0 deletions

11
.gitignore vendored Normal file
View File

@@ -0,0 +1,11 @@
node_modules/
logs/
node_modules.*
dist.zip
dist/
coverage/*
admin/node_modules/
# 临时文件目录(保留目录但忽略文件)
temp/*
!temp/.gitkeep

18
Dockerfile Normal file
View File

@@ -0,0 +1,18 @@
# 选择官方 Node 镜像
FROM node:20-alpine
# 设置工作目录
WORKDIR /app
# 复制 package.json 和 package-lock.json
COPY package*.json ./
# 安装依赖
RUN npm install --production
# 复制应用代码
COPY . .
# 暴露端口(根据你应用实际端口)
EXPOSE 9080
# 启动应用
CMD ["npm", "start"]

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2019 light
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

11
README.md Normal file
View File

@@ -0,0 +1,11 @@
# admin-framework.js 前端框架参考
http://js.light120.com/admin-framework.md
# node-core-framework 后端框架参考
http://js.light120.com/node-core-framework.md

View File

@@ -0,0 +1,101 @@
-- WechatAdminWeb 订阅模块业务表MySQL 8+
-- 执行前请确认库名;与 api/model/biz_*.js 字段一致
SET NAMES utf8mb4;
-- 业务用户(与 sys_user 后台账号区分)
CREATE TABLE IF NOT EXISTS `biz_users` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
`name` VARCHAR(100) NOT NULL DEFAULT '',
`mobile` VARCHAR(20) NULL DEFAULT NULL,
`email` VARCHAR(120) NULL DEFAULT NULL,
`company_name` VARCHAR(200) NULL DEFAULT NULL,
`status` ENUM('active', 'disabled') NOT NULL DEFAULT 'active',
`created_at` DATETIME NOT NULL,
`updated_at` DATETIME NOT NULL,
PRIMARY KEY (`id`),
KEY `idx_biz_users_mobile` (`mobile`),
KEY `idx_biz_users_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='业务用户';
-- 套餐
CREATE TABLE IF NOT EXISTS `biz_plans` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
`plan_code` VARCHAR(64) NOT NULL DEFAULT '',
`plan_name` VARCHAR(128) NOT NULL DEFAULT '',
`monthly_price` DECIMAL(12, 2) NOT NULL DEFAULT 0,
`auth_fee` DECIMAL(12, 2) NOT NULL DEFAULT 0,
`account_limit` INT NOT NULL DEFAULT 0,
`active_user_limit` INT NOT NULL DEFAULT 0,
`msg_quota` INT NOT NULL DEFAULT 0,
`mass_quota` INT NOT NULL DEFAULT 0,
`friend_quota` INT NOT NULL DEFAULT 0,
`sns_quota` INT NOT NULL DEFAULT 0,
`enabled_features` JSON NULL,
`status` ENUM('active', 'inactive') NOT NULL DEFAULT 'active',
`created_at` DATETIME NOT NULL,
`updated_at` DATETIME NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_biz_plans_code` (`plan_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='套餐';
-- 订阅实例
CREATE TABLE IF NOT EXISTS `biz_subscriptions` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
`user_id` BIGINT UNSIGNED NOT NULL,
`plan_id` BIGINT UNSIGNED NOT NULL,
`status` ENUM('pending', 'active', 'expired', 'cancelled') NOT NULL DEFAULT 'pending',
`start_time` DATETIME NOT NULL,
`end_time` DATETIME NOT NULL,
`renew_mode` ENUM('manual', 'auto') NOT NULL DEFAULT 'manual',
`payment_channel` ENUM('offline', 'pay_link') NULL DEFAULT NULL,
`payment_ref` VARCHAR(200) NULL DEFAULT NULL,
`created_at` DATETIME NOT NULL,
`updated_at` DATETIME NOT NULL,
PRIMARY KEY (`id`),
KEY `idx_biz_sub_user` (`user_id`),
KEY `idx_biz_sub_plan` (`plan_id`),
KEY `idx_biz_sub_status_end` (`status`, `end_time`),
CONSTRAINT `fk_biz_sub_user` FOREIGN KEY (`user_id`) REFERENCES `biz_users` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT `fk_biz_sub_plan` FOREIGN KEY (`plan_id`) REFERENCES `biz_plans` (`id`) ON DELETE RESTRICT ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='订阅';
-- API Token库内仅存 hashplan_id 冗余便于鉴权少联表)
CREATE TABLE IF NOT EXISTS `biz_api_tokens` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
`user_id` BIGINT UNSIGNED NOT NULL,
`plan_id` BIGINT UNSIGNED NULL DEFAULT NULL,
`token_name` VARCHAR(100) NOT NULL DEFAULT '',
`token_hash` VARCHAR(64) NOT NULL,
`status` ENUM('active', 'revoked', 'expired') NOT NULL DEFAULT 'active',
`expire_at` DATETIME NOT NULL,
`last_used_at` DATETIME NULL DEFAULT NULL,
`created_at` DATETIME NOT NULL,
`updated_at` DATETIME NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_biz_token_hash` (`token_hash`),
KEY `idx_biz_token_user` (`user_id`),
KEY `idx_biz_token_plan` (`plan_id`),
CONSTRAINT `fk_biz_token_user` FOREIGN KEY (`user_id`) REFERENCES `biz_users` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT `fk_biz_token_plan` FOREIGN KEY (`plan_id`) REFERENCES `biz_plans` (`id`) ON DELETE SET NULL ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='API Token';
-- 月用量
CREATE TABLE IF NOT EXISTS `biz_usage_monthly` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
`user_id` BIGINT UNSIGNED NOT NULL,
`plan_id` BIGINT UNSIGNED NOT NULL,
`stat_month` CHAR(7) NOT NULL COMMENT 'YYYY-MM',
`msg_count` INT NOT NULL DEFAULT 0,
`mass_count` INT NOT NULL DEFAULT 0,
`friend_count` INT NOT NULL DEFAULT 0,
`sns_count` INT NOT NULL DEFAULT 0,
`active_user_count` INT NOT NULL DEFAULT 0,
`created_at` DATETIME NOT NULL,
`updated_at` DATETIME NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_biz_usage_user_month` (`user_id`, `stat_month`),
KEY `idx_biz_usage_plan` (`plan_id`),
CONSTRAINT `fk_biz_usage_user` FOREIGN KEY (`user_id`) REFERENCES `biz_users` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT `fk_biz_usage_plan` FOREIGN KEY (`plan_id`) REFERENCES `biz_plans` (`id`) ON DELETE RESTRICT ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='月用量';

View File

@@ -0,0 +1,16 @@
-- 审计日志(关键操作留痕)
SET NAMES utf8mb4;
CREATE TABLE IF NOT EXISTS `biz_audit_log` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
`admin_user_id` BIGINT UNSIGNED NULL DEFAULT NULL COMMENT '后台操作者 sys_user.id可空',
`biz_user_id` BIGINT UNSIGNED NULL DEFAULT NULL COMMENT '相关业务用户',
`action` VARCHAR(64) NOT NULL COMMENT '动作标识',
`resource_type` VARCHAR(64) NOT NULL DEFAULT '',
`resource_id` BIGINT UNSIGNED NULL DEFAULT NULL,
`detail` JSON NULL,
`created_at` DATETIME NOT NULL,
PRIMARY KEY (`id`),
KEY `idx_biz_audit_action` (`action`),
KEY `idx_biz_audit_created` (`created_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='订阅模块审计';

View File

@@ -0,0 +1,840 @@
# 后端管理平台需求说明(仅订阅模块)
## 1. 范围
本期只做订阅模块,不考虑白标/OEM不做合同/财务系统。
包含:
- 用户管理(后台)
- 套餐管理
- 订阅开通、升级、续费、到期失效
- Token发放与吊销
- 额度校验与月用量统计
- 线下收款或第三方支付链接确认
不包含:
- 白标/OEM
- 分成系统
- 合同审批流
- 发票与财务对账系统
---
## 2. 角色
- 超级管理员:配置套餐、处理升级、强制开通/停用
- 运营创建用户、确认支付、开通订阅、发放Token
---
## 3. 数据模型(最小集)
## 3.1 用户表 `users`
- `id`
- `name`
- `mobile`
- `email`
- `company_name`
- `status`active/disabled
## 3.2 套餐表 `plans`
- `id`
- `plan_code`starter/pro/enterprise
- `plan_name`
- `monthly_price`
- `auth_fee`
- `account_limit`
- `active_user_limit`
- `msg_quota`
- `mass_quota`
- `friend_quota`
- `sns_quota`
- `enabled_features`json具体功能点
- `status`active/inactive
## 3.3 订阅表 `subscriptions`
- `id`
- `user_id`
- `plan_id`
- `status`pending/active/expired/cancelled
- `start_time`
- `end_time`
- `renew_mode`manual/auto
- `payment_channel`offline/pay_link
- `payment_ref`(线下流水号或第三方订单号)
## 3.4 Token表 `api_tokens`
- `id`
- `user_id`
- `token_name`
- `token_hash`(仅存哈希)
- `status`active/revoked/expired
- `expire_at`
- `last_used_at`
## 3.5 月用量表 `usage_monthly`
- `id`
- `user_id`
- `plan_id`
- `stat_month`YYYY-MM
- `msg_count`
- `mass_count`
- `friend_count`
- `sns_count`
- `active_user_count`
---
## 4. 核心流程
## 4.1 开通流程
1. 运营创建用户
2. 选择套餐
3. 记录支付方式(线下/支付链接)
4. 支付确认后创建订阅(`active`
5. 生成Token并展示一次明文
## 4.2 升级流程
1. 选择新套餐
2. 确认补差支付
3. 更新订阅的 `plan_id` 与生效时间
4. 新套餐额度立即生效或次月生效(可配置)
## 4.3 续费流程
1. 到期前提醒D-7/D-3/D-1
2. 支付确认后延长 `end_time`
3. 超期未续费则置为 `expired`
4. `expired` 状态禁止业务调用
## 4.4 Token鉴权流程每次请求
1. 校验Token是否存在、是否过期、是否吊销
2. 查询用户是否有 `active` 订阅
3. 校验功能是否在 `enabled_features`
4. 校验本月用量是否超套餐额度
5. 通过则放行,失败返回明确错误码
---
## 5. 功能需求
## 5.1 用户管理
- 新增/编辑/禁用用户
- 查询用户当前订阅状态
- 查询用户Token列表
## 5.2 套餐管理
- 新增/编辑套餐
- 配置套餐价格、额度、功能点
- 套餐上下线
## 5.3 订阅管理
- 开通订阅
- 升级订阅
- 续费订阅
- 手动取消订阅
- 到期自动失效任务
## 5.4 Token管理
- 创建Token只显示一次明文
- 吊销Token
- 设置Token有效期
- 查询最后使用时间
## 5.5 支付确认(轻量)
- 线下收款:运营手动确认并录入 `payment_ref`
- 支付链接:接收第三方回调后确认支付并开通
---
## 6. 错误码(建议)
- `TOKEN_INVALID`Token不存在
- `TOKEN_EXPIRED`Token已过期
- `TOKEN_REVOKED`Token已吊销
- `SUBSCRIPTION_INACTIVE`:无有效订阅
- `FEATURE_NOT_ALLOWED`:功能不在套餐内
- `QUOTA_EXCEEDED`:本月额度已用尽
---
## 7. 管理端APIMVP
## 7.1 用户
- `POST /admin/users`
- `GET /admin/users`
- `GET /admin/users/{id}`
- `PUT /admin/users/{id}`
- `POST /admin/users/{id}/disable`
## 7.2 套餐
- `POST /admin/plans`
- `GET /admin/plans`
- `PUT /admin/plans/{id}`
- `POST /admin/plans/{id}/toggle`
## 7.3 订阅
- `POST /admin/subscriptions/open`
- `POST /admin/subscriptions/upgrade`
- `POST /admin/subscriptions/renew`
- `POST /admin/subscriptions/cancel`
- `GET /admin/subscriptions/{user_id}`
## 7.4 Token
- `POST /admin/tokens`
- `GET /admin/tokens?user_id=`
- `POST /admin/tokens/{id}/revoke`
## 7.5 支付确认
- `POST /admin/payments/confirm-offline`
- `POST /admin/payments/confirm-link`
---
## 8. 定时任务
- 订阅到期扫描每天00:10执行更新 `expired` 状态
- 用量月结归档每月1日00:30执行
- 续费提醒任务每天09:00执行
---
## 9. 验收标准
- 可完成用户创建 -> 支付确认 -> 订阅开通 -> Token发放闭环
- 可完成升级与续费并正确生效
- 到期后能自动失效并阻断调用
- 功能与额度限制生效,错误码准确
- 支付链接回调或线下确认都可触发开通
---
## 10. 开发优先级
P0
- 用户管理
- 套餐管理
- 订阅开通/升级/续费/失效
- Token管理与鉴权
P1
- 支付回调确认
- 用量统计与额度限制
- 到期提醒
---
# 后端管理平台需求说明(轻量核心版)
## 1. 目标
搭建最小可用后台,只做四件事:
- 用户管理
- Token管理
- 套餐管理
- 开通/升级流程管理
不做V1不开发
- 合同管理
- 财务系统集成
- 分成系统
- 复杂风控引擎
收费方式:
- 线下打款或在线支付链接(第三方支付页面)
- 支付成功后由后台手动或回调触发开通
---
## 2. 核心角色
- 超管:全局配置、套餐配置、手动开通
- 运营用户资料维护、Token发放、套餐变更
- 客户可选后续仅查看自己的Token和套餐状态
---
## 3. 核心数据模型
## 3.1 用户
`users`
- id
- name
- mobile
- email
- company_name
- statusactive / disabled
- created_at
- updated_at
## 3.2 套餐
`plans`
- id
- plan_codestarter/pro/enterprise
- plan_name
- monthly_price
- auth_fee
- account_limit
- active_user_limit
- msg_quota
- mass_quota
- friend_quota
- sns_quota
- enabled_featuresjson数组存具体功能点
- statusactive / inactive
- created_at
- updated_at
## 3.3 用户套餐实例
`user_subscriptions`
- id
- user_id
- plan_id
- start_time
- end_time
- statuspending/active/expired/cancelled
- payment_channeloffline / pay_link
- payment_ref线下流水号或第三方订单号
- created_at
- updated_at
## 3.4 Token
`api_tokens`
- id
- user_id
- token_name
- token_hash仅存哈希
- statusactive/revoked/expired
- plan_id冗余便于鉴权
- expire_at
- last_used_at
- created_at
- updated_at
## 3.5 用量(按月聚合)
`usage_monthly`
- id
- user_id
- plan_id
- stat_monthYYYY-MM
- msg_count
- mass_count
- friend_count
- sns_count
- active_user_count
- updated_at
---
## 4. 功能需求MVP
## 4.1 用户管理
- 新增/编辑/禁用用户
- 查看用户当前套餐、到期时间、Token数量
- 用户搜索(手机号、公司名、状态)
## 4.2 套餐管理
- 新增/编辑套餐
- 配置套餐额度和功能点
- 套餐上下线active/inactive
- 查看套餐被多少用户使用
## 4.3 Token管理
- 为用户创建Token仅展示一次明文
- Token吊销revoke
- Token过期时间设置
- Token使用记录查看最后使用时间
鉴权规则(核心):
- 请求带Token
- 校验Token状态与是否过期
- 读取用户当前有效套餐
- 判断功能是否在套餐内
- 判断当月额度是否超限
## 4.4 开通与升级流程
### 开通流程
1. 创建用户
2. 选择套餐
3. 记录支付方式(线下/支付链接)
4. 支付成功后开通订阅
5. 生成Token并交付客户
### 升级流程
1. 选择新套餐
2. 记录补差支付
3. 生效新套餐(立即或次月)
4. Token权限自动按新套餐生效
### 续费流程
1. 到期前提醒D-7、D-3、D-1
2. 支付成功后延长套餐周期
3. 未支付则到期失效Token权限受限
---
## 5. 支付与开通方式(轻量)
支持两种方式:
- `offline`:线下收款,后台手动录入流水号后开通
- `pay_link`:调用第三方支付链接,回调成功后自动开通(或运营手动确认)
V1要求
- 只保存支付结果与订单号
- 不做复杂账单系统
---
## 6. 最小风控与安全
- Token只存哈希不存明文
- 超管/运营所有关键操作写审计日志
- 单用户默认Token数量上限如5个
- 支持一键吊销全部Token
- 超额度后返回明确错误码(不做复杂策略引擎)
---
## 7. 接口清单(管理端)
## 7.1 用户
- `POST /admin/users` 创建用户
- `GET /admin/users` 用户列表
- `GET /admin/users/{id}` 用户详情
- `PUT /admin/users/{id}` 更新用户
- `POST /admin/users/{id}/disable` 禁用用户
## 7.2 套餐
- `POST /admin/plans` 创建套餐
- `GET /admin/plans` 套餐列表
- `PUT /admin/plans/{id}` 更新套餐
- `POST /admin/plans/{id}/toggle` 上下线套餐
## 7.3 订阅
- `POST /admin/subscriptions/open` 开通套餐
- `POST /admin/subscriptions/upgrade` 升级套餐
- `POST /admin/subscriptions/renew` 续费套餐
- `GET /admin/subscriptions/{user_id}` 查询用户订阅
## 7.4 Token
- `POST /admin/tokens` 创建Token
- `GET /admin/tokens?user_id=` Token列表
- `POST /admin/tokens/{id}/revoke` 吊销Token
## 7.5 支付记录
- `POST /admin/payments/confirm-offline` 线下收款确认
- `POST /admin/payments/confirm-link` 支付链接回调确认
---
## 8. 验收标准(上线即用)
- 可创建用户并成功开通任一套餐
- 可生成Token并用Token通过鉴权
- 套餐功能和额度能正确限制请求
- 可完成升级、续费、到期失效流程
- 支付成功后可触发开通(手动或回调)
---
## 9. 开发优先级与周期建议
P0第1周
- 用户管理
- 套餐管理
- 订阅开通
- Token创建与鉴权
P1第2周
- 升级/续费流程
- 线下支付确认与支付链接回调
- 月用量聚合与额度限制
P2第3周
- 到期提醒
- 审计日志页面
- 基础运营看板
---
# 微信自动化平台后端管理需求说明V1
## 1. 目标与范围
建设一套后端管理平台,支撑 `方案.md``功能执行文档-报价方案配套.md` 的落地执行,覆盖:
- 客户开通与版本管理
- 功能开关与白名单控制
- 额度管理与超量计费
- 授权码生命周期管理
- 账单、对账、分成结算
- 风险控制与违规处置
不在V1范围
- 客户前台门户(仅后台运营端)
- 财务开票系统深度集成(先导出报表)
---
## 2. 角色与权限
## 2.1 角色
- 超级管理员:全局配置、价格策略、风控策略
- 商务运营:客户开通、版本变更、合同与授权
- 交付运营:功能开关、额度配置、验收记录
- 财务:账单确认、对账、分成结算
- 风控:告警处理、停用/解封、违规记录
## 2.2 权限原则
- 最小权限原则
- 核心动作(停服、改价、授权升级)需二次确认与审计日志
- 所有配置变更可追溯(操作者、时间、变更前后)
---
## 3. 核心业务对象(数据模型)
## 3.1 客户与合同
- `customer`:客户主体信息(公司/个人、联系人、状态)
- `contract`:合同编号、版本、期限、签约类型、分成比例、补充条款
- `service_instance`:客户服务实例(当前版本、生效时间、到期时间)
## 3.2 版本与功能
- `plan`:初级/高级/定制/白标
- `feature_catalog`:功能目录(按具体功能点,不按抽象模块)
- `plan_feature_map`:版本与功能映射(是否默认开通)
- `feature_switch`:客户级功能开关(开/关、原因、操作者)
## 3.3 额度与计费
- `quota_policy`:版本默认额度(消息、群发、加好友、朋友圈、企业微信)
- `customer_quota`:客户当前额度(可覆盖默认值)
- `usage_daily` / `usage_monthly`:按客户、按计费项聚合用量
- `price_policy`:单价策略(默认价、阶梯价、生效时间)
- `billing_statement`:月账单(固定费、超量费、人头费、分成)
## 3.4 授权与风控
- `license_key`:授权码及状态(生成、激活、禁用、延期、删除)
- `risk_rule`:风控规则(阈值、频率、异常模式)
- `risk_event`:风险事件记录(级别、动作、处理结果)
- `penalty_action`:处罚动作(限流、降级、停服、解封)
---
## 4. 功能模块需求
## 4.1 客户开通中心
功能点:
- 创建客户档案
- 绑定合同与版本
- 设置账号上限、活跃终端用户上限
- 初始化功能开关与额度
- 生成开通确认单
校验规则:
- 无合同不得开通付费版本
- 开通版本必须匹配授权级别
- 对外收费客户必须配置分成策略
---
## 4.2 版本与功能管理
功能点:
- 维护版本基础配置(初级/高级/定制/白标)
- 维护“具体功能点”目录与分组
- 一键应用版本默认功能到客户
- 客户级功能差异化调整(例外开关)
关键要求:
- 必须支持“包含/禁开”双清单导出(用于交付验收)
- 功能变更需记录变更单号与审批人
---
## 4.3 额度与限流管理
功能点:
- 配置套餐默认额度
- 按客户调整额度
- 配置阈值告警80%、100%、120%
- 配置超额后策略(仅计费 / 限流 / 禁止高风险动作)
关键要求:
- 日级、月级双维度统计
- 支持手动补偿额度(需审计)
---
## 4.4 计费与账单中心
功能点:
- 固定费用:月费 + 授权费
- 用量费用:按计费项单价计算
- 用户规模费用超过100后按1元/人/月(可配置)
- 分成费用:按合同分成比例计算
- 生成月账单、账单确认、导出对账单
计算规则V1
- 月账单金额 = 固定费 + 超量费 + 人头费 + 分成费
- 人头费基数 = max(活跃终端用户-100, 0)
- 活跃终端用户按唯一ID月去重
---
## 4.5 授权码管理
功能点:
- 生成授权码(不同有效期)
- 激活状态查询
- 禁用、删除、延期
- 授权状态同步任务
关键要求:
- 授权码与客户主体绑定
- 禁用后相关能力按策略自动降级/停用
---
## 4.6 分成与结算管理
功能点:
- 设置客户分成比例10%/15%/20%或合同自定义)
- 录入/同步客户营收数据
- 自动计算应结分成
- 月度结算单导出
关键要求:
- 分成口径可配置(净营收/归因营收)
- 允许人工复核后确认入账
---
## 4.7 风控与违规处置
功能点:
- 风控规则配置(频率阈值、异常行为模式)
- 告警中心(低/中/高风险)
- 自动处罚策略(限流、冻结高风险功能、停服)
- 违规记录与解封流程
关键要求:
- 处罚动作必须可回溯
- 高风险停服需双人审批(可配置)
---
## 4.8 审计与操作日志
必须记录:
- 谁在何时改了什么
- 改前值/改后值
- 操作来源(后台/任务/接口)
- 关联客户、合同、账单编号
---
## 5. 核心流程(状态机)
## 5.1 客户生命周期
- `待签约 -> 已签约 -> 待开通 -> 已开通 -> 运行中 -> 欠费限制 -> 暂停服务 -> 终止`
## 5.2 版本变更流程
- 提交升级/降级申请 -> 商务审批 -> 配置预览 -> 生效执行 -> 通知客户 -> 留档
## 5.3 月结流程
- 月末汇总用量 -> 生成预账单 -> 财务复核 -> 账单确认 -> 推送客户 -> 回款核销
---
## 6. 配置字典(建议)
## 6.1 计费项编码
- `MSG_CALL`:消息调用
- `MASS_TASK`:群发任务
- `ADD_FRIEND`:加好友动作
- `SNS_ACTION`:朋友圈动作
- `QY_ACTION`:企业微信动作
- `ACTIVE_USER`:活跃终端用户
## 6.2 风险等级
- `LOW`:告警
- `MEDIUM`:限流或冻结部分能力
- `HIGH`:停服并人工复核
---
## 7. 报表与看板需求
## 7.1 运营看板
- 在服客户数(按版本)
- 当月升级/降级数量
- 功能开关变更次数
- 告警事件数量
## 7.2 财务看板
- MRR、ARR
- 超量收入占比
- 人头费收入占比
- 分成收入占比
- 回款率与逾期率
## 7.3 客户视图
- 客户当前版本与到期时间
- 功能已开通清单
- 本月额度、已用量、超量预估
- 本月预估账单
---
## 8. 非功能性要求
- 可用性:后台可用性 >= 99.9%
- 审计性关键操作100%留痕
- 安全性:权限隔离、敏感操作二次确认
- 性能日汇总任务在60分钟内完成
---
## 9. MVP开发优先级
P0必须
- 客户开通中心
- 版本/功能开关管理
- 额度管理
- 用量统计
- 账单生成
- 授权码管理
- 审计日志
P1次优先
- 分成结算
- 风控自动处置
- 看板与报表
P2后续
- 工作流审批引擎
- 财务系统对接
- 客户自助门户
---
## 10. 验收标准
## 10.1 功能验收
- 能按客户版本一键开通并生效
- 能对单客户进行功能级开关控制
- 能正确统计月用量并生成账单
- 能处理授权码生成、禁用、延期、查询
## 10.2 计费验收
- 对同一客户可复算账单结果误差为0
- 活跃终端用户去重规则正确
- 超量计费与阶梯价计算正确
## 10.3 风控验收
- 触发阈值后自动生成告警
- 可执行限流/停服动作并记录日志
---
## 11. 建议交付物
- 后台原型图(客户、版本、计费、风控四大页面)
- 数据库DDL初稿
- 后台API清单管理端
- 月账单计算任务说明
- 上线回滚预案
---

View File

@@ -0,0 +1,162 @@
# WechatAdminWeb — 订阅模块后台实施计划
> 依据:`_docs/后端管理平台需求说明.md` 中 **「订阅模块 / 轻量核心版」** 与 **§7 管理端 APIMVP**;技术栈以仓库现有 **node-core-framework** + **admin-framework.js** 为准。
> 说明:需求文档后半部分「微信自动化平台 V1合同/账单/分成/风控等)」**不在本期范围**,若后续要扩展,可单独立项。
---
## 1. 范围与验收对齐
### 1.1 本期必做P0
| 模块 | 内容 |
|------|------|
| 用户管理 | 增删改查、禁用;列表筛选(手机/公司/状态);详情含当前订阅与 Token 概要 |
| 套餐管理 | CRUD、上下线、额度与 `enabled_features`JSON |
| 订阅 | 开通、升级、续费、取消;按 `user_id` 查询;状态机 `pending/active/expired/cancelled` |
| Token | 创建(仅一次明文)、吊销、有效期、`last_used_at`;单用户数量上限(如 5 |
| 对外鉴权 | Token 校验 + 有效订阅 + 功能点 + 月额度;统一错误码 |
### 1.2 本期次优先P1
| 模块 | 内容 |
|------|------|
| 支付 | `confirm-offline``confirm-link`(录入 `payment_ref` / 模拟回调开通) |
| 用量与额度 | `usage_monthly` 聚合、调用侧上报或管理端录入;超配额返回 `QUOTA_EXCEEDED` |
| 定时任务 | 到期扫描、月结归档、续费提醒D-7/D-3/D-1 |
### 1.3 可选增强P2
- 审计日志表 + 关键操作写入(需求「最小风控与安全」)
- 一键吊销某用户全部 Token
- 简单运营看板(用户数、到期分布)
### 1.4 数据表命名统一
需求中同时出现 `subscriptions``user_subscriptions`。实施时 **固定一张业务订阅表**(建议表名 `biz_subscriptions``user_subscriptions`,与现有命名风格一致),在 DDL 与文档中只保留一种,避免混用。
`api_tokens` 轻量版建议含 **`plan_id` 冗余**便于鉴权时少联表与需求「3.4 Token」轻量版一致。
---
## 2. 技术约定(与仓库现状对齐)
### 2.1 后端
- **入口**`app.js``Framework.init``framework/node-core-framework.js`)。
- **管理端 API 前缀**`config/framework.config.js``apiPaths` 已配置为 **`/admin_api`** + `authType: 'admin'`
- 需求文档中的 `/admin/...` 在实现时映射为 **`/admin_api/...`**(或在网关层做重写),**Swagger/前端 axios 基路径需统一**。
- **模型**`api/model/*.js`Sequelize `db.define` 风格(参考 `api/model/sys_user.js`)。
- **控制器**`api/controller_admin/*.js`,导出形如 `"METHOD /path": async (ctx) => {}`(参考 `api/controller_admin/sys_file.js`)。
- **关联**`config/model.associations.js` 中注册 `users` / `plans` / `subscriptions` / `api_tokens` / `usage_monthly``belongsTo`/`hasMany`
### 2.2 需先修复的阻塞项
| 问题 | 处理 |
|------|------|
| `app.js` 引用 `./config/model.associations.wms.js`,仓库中 **不存在** | 改为引用 `./config/model.associations.js`,或在 `config` 下增加兼容 re-export保证 `npm start` 可启动 |
### 2.3 管理端前端
- **入口**`admin/src/main.js``AdminFramework.createApp` + `componentMap``admin/src/router/component-map.js`)。
- **页面**:为「用户 / 套餐 / 订阅 / Token / 支付确认」各增 Vue 页,并在 `componentMap` 注册;列表/表单模式对齐现有 `admin-framework``test/test.vue` 用法。
- **接口封装**`admin/src/api/` 下新增模块,**baseURL 指向 `admin_api`**(与 `admin/config``apiUrl` 一致)。
### 2.4 定时任务
- `middleware/schedule.js``init()` 内用 `node-schedule` 注册:
- 每天 **00:10** — 订阅到期扫描 → `expired`
- 每月 **1 日 00:30** — 用量月结归档(如有需要)
- 每天 **09:00** — 续费提醒写通知表或日志MVP 可先日志)
---
## 3. 数据层实施清单
### 3.1 DDLMySQL
新建迁移或 SQL 脚本(建议 `_docs/sql/``migrations/`,与团队习惯一致)包含:
- `biz_users`(或 `users`,注意与 `sys_user` 后台账号区分 — **建议业务客户表用前缀 `biz_`**,避免与系统用户混淆)
- `biz_plans`
- `biz_subscriptions`(字段含 `renew_mode``payment_channel``payment_ref` 等)
- `biz_api_tokens``token_hash``plan_id` 可选冗余)
- `biz_usage_monthly``stat_month` YYYY-MM
### 3.2 Sequelize 模型文件
每个表一个 `api/model/biz_*.js`,字段类型、注释与需求一致;`enabled_features``JSON``TEXT` + 序列化。
### 3.3 安全
- Token 明文仅创建接口返回一次;库内只存 **hash**(如 SHA-256 + salt与框架内 crypto 工具一致)。
- 管理端接口走 `admin` 鉴权;对外鉴权接口放 `api/controller_front` 或单独 `prefix`**放入 allowUrls 或 Header 鉴权**,与需求「每次请求」流程一致。
---
## 4. API 实施清单(路径以 `/admin_api` 为准)
需求 §7 映射如下(**实际路由字符串以框架注册为准**
| 需求路径 | 实现前缀 |
|----------|----------|
| `POST /admin/users` | `POST /admin_api/users``/admin_api/biz_users` |
| `GET /admin/users` | `GET /admin_api/users` |
| `GET /admin/users/{id}` | `GET /admin_api/users/:id` |
| `PUT /admin/users/{id}` | `PUT /admin_api/users/:id` |
| `POST /admin/users/{id}/disable` | `POST /admin_api/users/:id/disable` |
| 套餐 / 订阅 / Token / 支付 | 同上模式 |
**对外鉴权(非管理端)**建议单独一组,例如:
- `POST /api/auth/verify``GET /api/auth/context`:入参 Token + `feature` + 可选用量上报字段,返回用户上下文或错误码。
错误码与需求 §6 一致:`TOKEN_INVALID``TOKEN_EXPIRED``TOKEN_REVOKED``SUBSCRIPTION_INACTIVE``FEATURE_NOT_ALLOWED``QUOTA_EXCEEDED`
---
## 5. 前端页面与菜单
| 页面 | 路由 component key示例 | 功能 |
|------|------------------------------|------|
| 用户列表/编辑 | `biz/user` | 表格 + 搜索 + 抽屉表单 |
| 套餐列表/编辑 | `biz/plan` | JSON 功能点编辑器(简易 textarea 或 key-value |
| 订阅操作 | `biz/subscription` | 开通/升级/续费/取消向导或表单 |
| Token | `biz/token` | 列表、创建(展示一次明文)、吊销 |
| 支付确认 | `biz/payment` | 线下确认、链接确认表单 |
菜单数据若来自后端 `sys_menu`,需在库中插入对应菜单项,`component``component-map.js` **key 一致**
---
## 6. 实施顺序(建议迭代)
1. **迭代 A**:修复 `model.associations` 引用;建表 + 模型 + associations用户 + 套餐 CRUD API 与页面。
2. **迭代 B**:订阅状态机 + 开通/升级/续费/取消 API + 页面支付确认打通「pending → active」。
3. **迭代 C**Token 创建/吊销/哈希;对外鉴权接口 + 错误码;用量表与额度校验。
4. **迭代 D**定时任务到期扫描、提醒Swagger 补全;联调验收用例(需求 §9
---
## 7. 交付物
- 可运行的后端 + 管理端前端,完成「创建用户 → 支付确认 → 订阅生效 → Token 发放 → 鉴权与额度」闭环。
- 本文档同目录可补充 **`_docs/sql/001_biz_schema.sql`**(实施时产出)。
- API 文档:框架自带 `/api/docs`,需保证新接口有 Swagger 注解或框架要求的注释格式。
---
## 8. 参考文件(仓库内)
| 说明 | 路径 |
|------|------|
| 框架入口 | `app.js``config/framework.config.js` |
| 管理端控制器 | `api/controller_admin/sys_file.js` |
| 模型示例 | `api/model/sys_user.js` |
| 定时任务壳 | `middleware/schedule.js` |
| 前端挂载 | `admin/src/main.js``admin/src/router/component-map.js` |
| 需求原文 | `_docs/后端管理平台需求说明.md` |
---
*文档版本v1 | 与 README 中 admin-framework / node-core-framework 在线文档对照开发。*

1
_license/license.lic Normal file
View File

@@ -0,0 +1 @@
eyJ2ZXJzaW9uIjoiMy4wIiwiZGF0YSI6eyJyZWdpc3Rlcl9jb2RlIjoiZXlKMklqb2lNeTR4SWl3aWFDSTZJakpsTm1abVpURmhaVEU1WmpnelpXSXlNbVk0T0dFNFpHVm1NVE0zT0Rrd1pERXpZamcwWkdNd05EYzNaall5TURkbU5tSmxNalk1TkRJME1qQTJaVFlpTENKeklqb2lPVEkyT0dRNU1EY3paV0UwTWpnNVpHVXdNRFF6TldOaU5EazBOVFU1TnpraUxDSjBJam94TnpVNU5ERTJNRFUyTENKdUlqb2lNbU00WkRVME1ETTJNR0k1T1RoaE5DSXNJbXNpT2lJMk5EUmpaR0ZoTWlJc0ltTWlPaUptTVdNek5tVmpaREptWmpjMFpXUmpOVFJrTlRrNE5HTTNaV1k1TjJJNFpUQTJaVFV6WVRsbU1Ua3hOV0ZrTURRME5tUTFNRFk0WldSbE5ETmpZMkV6SWl3aWJTSTZJbVF4TVROaFptSXlOREE1T1RsaE5qWWlmUT09IiwidmVyc2lvbiI6IjMuMCIsInJlbWFyayI6IiIsImlzc3VlciI6IlBsYXRmb3JtV2ViIExpY2Vuc2UgU2VydmVyIiwiZW5jcnlwdGVkX2RhdGEiOiJMcXQ1MnpTT2dFdlR6dFA5T0FCTlU1NmY2L3JIMlpNR2puQnA5UXgxb2FkcW1YMXBISThkdEVVWWVET29IQ0Z5Q0N2b2p3TkVpV0R4YmN2RCtMMDAwSitOMXF5WjBNZGdPanZoSGRFbG0wVWxlYVNuNjR6QWU4M1FBelNOUGlIRWJWbEcvSk4ySUpHdVg2anhFaktsZCtCNGVVWXdoa1VVakFsUnVQL0lOVUFZZXplYW1sM0x2RmRTQW4xRnV2QXMiLCJhZXNfaXYiOiJpRkQ0VGhrdEFBcTY3emdsWlR1VStRPT0iLCJub25jZSI6ImRiYjM2ZTg0NThiNTEzMDJkYzNhYTU4NDI1MTY1M2JlIiwia2V5X2lkIjoiNjQ0Y2RhYTItZmRmNy00MjEzLTgyNWItZDkxOWU4YjQ0Mzg4IiwiZGF0YV9oYXNoIjoiYmUzZmFjZDkyNWVjM2FmMjQ3ZjQ3NTZjMzUzMWJlYTM1ZTMyZmZjYTA5MTg0ZDM2Y2E3ZmFmOWJkMWRmYzI3NiIsImV4cGlyZV90aW1lX3B1YmxpYyI6IjIwMjYtMTItMDVUMDg6NTk6MzQuNjQ0WiIsImNyZWF0ZWRfdGltZV9wdWJsaWMiOiIyMDI1LTEyLTA1VDA4OjU5OjM0LjY0NVoifSwiZW5jcnlwdGVkX2tleSI6IlVGZEpzUE5DVW9pb3hNTHE1eFNvaklveHR1c3FkMXhsY0hLODZRS01tT0hkRzZpY3dYd0tDNEx4dDYxTHNMVHY5VEZqN0V5NldGNkVtVjZibDMvK21DKzZsdmRBZnNPL2ZTR0xkcDdmMUhXcC9mUVlqSGJIMEhnZUhjd3ZxUWppajRha2hxdkJsRVN6dWhUZTF2cmxzRDhOZk9aaUlvb0llTHp6aTBoVVpHSVFlTC85ek1ZTjhNQXlkczRkZk9VaGRtTGh0QkVYcHlFR2lkcVRkT28zODh2Z0pKamlLdm9FdU9JNm9tN0NPWFdPNkRvQmxsMkllU0EvVmJzZjE3cm1wbGVyYmtscXlKYktJQXVpZDlFKzNvYms4ZVhHaldpNHVWKzZXNW13anhNMzR5YTBkTEtrcGtlR2J6cXpBTjJmTFJLaWJ3Y0kvU3F2VkU5WnFhdzB6YnlhNHJZczFhVGVWdGp0NXcvQkxwUm9RbzJ2TE9Xdnd1N1R0Nko5U1VRUVNqK1JhVnZzTkQrUmZOdHlmbmd3VlM1ZUdhcEpaRHJOSVlUOHVHZW9qZDRaQjFTdnJyVnh2Ym10YW5EcHYrYkxnZTVPZVNXWCt4M2ErME03Wmd3NE02aTBXRmNoTyticnVFSU9RSkZ4K3g0ZVFoK25oaFFJcnNRaHZoRkpBRXRFTjdhUXludU5yeFNDU1FqdGpnZnhuSnBEOXpTc2w5VmNxanNlZDF5SUJPM3hsUkh4MUdFenFNbG1iTHlLNkpBNlBUcUIwWUJoN0FmM2pkaHZhSDc0b3NRV3RZcjg3dC9oK1dwT2VLL1pDTDJ3WFdKM1Fiam9SVXRMYTFWc3dxZnQweTd4VXBBNzZJMnlaYzdDTHpwdzR0MFl0cjhrQzZLemdhUzhxbkV1cG1vPSIsInNpZ25hdHVyZSI6IlVTNEFCVWwvSm9vZng4MjZzTVBSd2FDOTdscnhaeHJqOWEwT1g3Wm0xSkg0T2lQai9JY1BKVCtUZngyNzZpZkpNRGVwV2pCSFdFbVBWLzRHbDkxZnF3MTAwdk9EejZMQlBMY3doWkFiMzdGZWp0aEwwbnJBZERhZ2NqNUJOYkt6NTREYVJaZzlxNS9EMDlpVlltM2Y1YVFGckJxdS9oRHdFSGFrZGVBelF3eW5KOGxYWVcvYkZLN0ZieU9xakFlbDNMcjNBSTZ1Yzd6dDdmZDNKNHNGTkdqVjdMOWdmSi96eE1tRHdVV3BqdEN3d3RmUmwzNkFkbzl4MzNJbVNERkRvVWlJWWpWeXBUSVhERFlUZGJWSjNhMkEySzgxMUZteG4rRGhqVEJHMm80ZVpkbFJWZnRZcmVwREtUNS9PcjdRK2lxdG0rTEUyVTBNbDBDMXEzeFpaMitKM1RVM3JXTE9LTldIS3hCZk9seFJib0NxclRWcHpoZnhrOCthanRhSGMrWVU1UCsxYUxXN2ZaOEN6dCs5Z1ZMak1MQUI5SEZyRVAyNnFlOTRydDNIOGZtUlR6T3I3bUZsMUZic2dtRkRENkdkdmRuQmUrU09PSGdVQWJQYkI0TTBQcTRlSHJRTWZQSFpRSTRnd3JXY1p4QjgydFd2ZGdWTldHbVlWT0VxVVAxazVhS29wcFI2L2RGZnVmNmZOc3JKc1lWTVhCVWdPODJDWmQ0RzBDUTRDdllFQTVYa0xQUFpKSWlMdHEzQ2Y5eTdxazdidnVSejNMemVMWGhtMUk1ZHVoU2xENjZXNVFLWm9pZlpmYzJGYkwyMVpkV1Y0aGc0UllDRVRidk5MQkpOM092QlpQbTRYZUJmSlZnTGtkc2pXckFocWNVY0dxS243TGttc1U0PSIsInRpbWVzdGFtcCI6MTc2NDkyNTE3NH0=

14
_license/public_key.pem Normal file
View File

@@ -0,0 +1,14 @@
-----BEGIN PUBLIC KEY-----
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA5qcNLrwgngGJMqkHhY4b
KCeS1HZegxM744fRtdrnWNVA3JwYASc52aokSQh0ig9SKN9k1zRs3L7N4cF4i9kE
AfW/2c+yiaMmbX6LW3Wi+yRH2jvTbpj1GkB/9Lsa+OEvdqYaeMiBEVoHS7FZUEaV
dzqTqrikUfql3htEhGCI9CGqZFoi8dz0GGKqDpqX7380pbST5Qgi9N4ZQLRVcmOP
596xYMoXdfufJ4em+FftYT5Q1rDt42lJhO+UrENORwTrGwCJVmLtIWiHfiCxeUzx
5Ft/xOKacndR86L4CmKLekVjejQSo+4Ge8j/BEdVUWY1tMlUFTC8aUTeFE2yA6dt
FkX3dQzgEOlRUifLjalXxLmxPY77N+mcuDzjaRomdHdxoGZsRYlS8yHL74rixSRa
U9JOVL9i8csLmJarzmYx6jsl4sSDbcDdZHxC2AbdGdDHV5/Zr+a8m8B6PW2nArgB
bTNKVx9g8aj4n3jf3NGzRqW/TwNifY4xb6BrbeNTlhXl/9+RCvvmCZZYK8JKus55
3cvBvrLUBQdpkk9JwIzmEQZoitD8g4CB/2tKsKvfiwlQUK44HNfWE+cxiqtyXL+I
shRJkwYbt0CQsXmU5F5j/prOPiJZjjlk7jqSLZLyJ99vMMm0+Iw7kozweGs3zUct
dOvKFUYgxdSaMjTiMOXdcN0CAwEAAQ==
-----END PUBLIC KEY-----

View File

@@ -0,0 +1 @@
eyJ2IjoiMy4xIiwiaCI6IjJlNmZmZTFhZTE5ZjgzZWIyMmY4OGE4ZGVmMTM3ODkwZDEzYjg0ZGMwNDc3ZjYyMDdmNmJlMjY5NDI0MjA2ZTYiLCJzIjoiOTI2OGQ5MDczZWE0Mjg5ZGUwMDQzNWNiNDk0NTU5NzkiLCJ0IjoxNzU5NDE2MDU2LCJuIjoiMmM4ZDU0MDM2MGI5OThhNCIsImsiOiI2NDRjZGFhMiIsImMiOiJmMWMzNmVjZDJmZjc0ZWRjNTRkNTk4NGM3ZWY5N2I4ZTA2ZTUzYTlmMTkxNWFkMDQ0NmQ1MDY4ZWRlNDNjY2EzIiwibSI6ImQxMTNhZmIyNDA5OTlhNjYifQ==

14
admin/.babelrc Normal file
View File

@@ -0,0 +1,14 @@
{
"presets": [
[
"@babel/preset-env",
{
"modules": false,
"targets": {
"browsers": ["> 1%", "last 2 versions", "not ie <= 8"]
}
}
]
]
}

2
admin/.env.prod Normal file
View File

@@ -0,0 +1,2 @@
# 生产环境:与 config/index.js 中 productionConfig 对应,按需修改域名
BUILD_ENV=prod

2
admin/.env.sit Normal file
View File

@@ -0,0 +1,2 @@
# SIT 环境:与 config/index.js 中 sitConfig 对应,按需修改域名
BUILD_ENV=sit

24
admin/.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
# 依赖
node_modules/
# 构建输出
dist/
# 日志
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# 编辑器
.vscode/
.idea/
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# 系统文件
.DS_Store
Thumbs.db

251
admin/README.md Normal file
View File

@@ -0,0 +1,251 @@
# Admin Framework Demo
本目录包含 Admin Framework 的使用示例,提供两种使用方式:
## 📁 文件说明
### 🌐 CDN 版本(快速体验)
- **index.html** - 基础示例CDN
- **advanced.html** - 高级示例CDN
适合快速体验,所有依赖从 CDN 加载,无需安装。
### 💻 本地开发版本(推荐开发使用)
- **src/** - 源代码目录
- **main.js** - 基础示例入口
- **main-advanced.js** - 高级示例入口
- **components/** - 自定义组件
- **package.json** - 依赖配置
- **webpack.config.js** - 构建配置
所有依赖本地安装,支持热更新,适合开发调试。
---
## 🚀 使用方式
### 方式一CDN 版本(快速体验)
#### index.html - 基础示例
最简单的使用示例,展示如何:
- 引入必要的依赖
- 初始化框架
- 创建基本应用
#### advanced.html - 高级示例
完整的使用示例,展示如何:
- 添加自定义页面组件
- 注册自定义 Vuex 模块
- 添加自定义路由
- 配置路由守卫
- 配置 Axios 拦截器
- 使用组件映射
### 方式二:本地开发版本(推荐)
查看详细文档:[README-LOCAL.md](./README-LOCAL.md)
快速开始:
```bash
# 1. 构建框架(在项目根目录)
cd ..
npm run build
# 2. 安装 demo 依赖
cd demo
npm install
# 3. 启动开发服务器
npm run dev
```
## 使用步骤
### 1. 构建框架
首先需要构建 admin-framework
```bash
# 生产构建(压缩,无 sourcemap
npm run build
# 开发构建(不压缩,有 sourcemap
npm run build:dev
```
### 2. 启动示例
有以下几种方式启动示例:
#### 方式一:使用 Live Server推荐
1. 安装 VS Code 的 Live Server 插件
2. 右键 `index.html``advanced.html`
3. 选择 "Open with Live Server"
#### 方式二:使用 HTTP 服务器
```bash
# 安装 http-server
npm install -g http-server
# 在项目根目录运行
http-server
# 访问
# http://localhost:8080/demo/index.html
# http://localhost:8080/demo/advanced.html
```
#### 方式三:直接打开
- 双击 HTML 文件在浏览器中打开
- 注意:某些功能可能因跨域限制无法使用
## 配置说明
### 基本配置
```javascript
const config = {
title: '系统标题',
apiUrl: 'http://your-api.com/api/', // API 基础地址
uploadUrl: 'http://your-api.com/api/upload' // 上传接口地址
}
```
### 初始化框架
```javascript
framework.install(Vue, {
config: config, // 配置对象
ViewUI: iview, // iView 实例
VueRouter: VueRouter, // Vue Router
Vuex: Vuex, // Vuex
createPersistedState: null, // Vuex 持久化插件(可选)
componentMap: {} // 自定义组件映射
})
```
## 内置功能
### 1. 系统页面
- **登录页面**: `/login`
- **首页**: `/home`
- **错误页面**: `/401`, `/404`, `/500`
### 2. 系统管理
- **用户管理**: 系统用户的增删改查
- **角色管理**: 角色权限管理
- **菜单管理**: 动态菜单配置
- **日志管理**: 系统操作日志
### 3. 高级功能
- **动态表单**: 基于配置生成表单
- **动态表格**: 可配置的数据表格
- **文件上传**: 单文件/多文件上传
- **富文本编辑器**: WangEditor
- **代码编辑器**: Ace Editor
## API 使用
### HTTP 请求
```javascript
// GET 请求
framework.http.get('/api/users').then(res => {
console.log(res.data)
})
// POST 请求
framework.http.post('/api/users', {
name: '张三',
age: 25
}).then(res => {
console.log(res.data)
})
// 在组件中使用
this.$http.get('/api/users').then(res => {
console.log(res.data)
})
```
### 工具函数
```javascript
// 使用框架提供的工具函数
const tools = framework.tools
// 日期格式化
tools.formatDate(new Date(), 'yyyy-MM-dd HH:mm:ss')
// 深拷贝
tools.deepClone(obj)
// 防抖
tools.debounce(fn, 500)
// 节流
tools.throttle(fn, 500)
```
### UI 工具
```javascript
// 使用 UI 工具
const uiTool = framework.uiTool
// 成功提示
window.framework.uiTool.success('操作成功')
// 错误提示
window.framework.uiTool.error('操作失败')
// 确认对话框
window.framework.uiTool.confirm('确定删除吗?').then(() => {
// 确认后的操作
})
```
## 常见问题
### 1. 依赖库版本
确保使用以下版本的依赖库:
- Vue: 2.6.x
- Vue Router: 3.x
- Vuex: 3.x
- iView (view-design): 4.x
- Axios: 0.21.x+
### 2. 路径问题
如果无法加载 admin-framework.js检查路径是否正确
```html
<!-- 确保路径指向正确的文件 -->
<script src="../dist/admin-framework.js"></script>
```
### 3. API 地址
记得修改配置中的 API 地址为实际的后端地址:
```javascript
const config = {
apiUrl: 'http://your-real-api.com/api/',
uploadUrl: 'http://your-real-api.com/api/upload'
}
```
### 4. 跨域问题
如果遇到跨域问题,需要配置后端 CORS 或使用代理。
## 开发建议
1. **开发时使用 build:dev**
- 生成 sourcemap方便调试
- 代码不压缩,易读
2. **生产时使用 build**
- 代码压缩,体积小
- 无 sourcemap安全
3. **使用浏览器调试工具**
```javascript
// 所有实例都挂载到 window 上,方便调试
window.app // Vue 实例
window.framework // 框架实例
```
## 更多信息
查看完整文档:`../_doc/完整使用文档.md`

248
admin/config/README.md Normal file
View File

@@ -0,0 +1,248 @@
# Admin 前端配置说明
## 📁 配置文件结构
```
admin/
├── config/
│ ├── index.js # 主配置文件(支持多环境)
│ └── README.md # 配置说明文档(本文件)
├── env.development # 开发环境变量
├── env.test # 测试环境变量
└── env.production # 生产环境变量
```
---
## ⚙️ 配置项说明
### 基础配置
| 配置项 | 类型 | 默认值 | 说明 |
|--------|------|--------|------|
| `title` | String | 'Tennis管理系统' | 系统标题 |
| `apiUrl` | String | - | 后端 API 地址 |
| `uploadUrl` | String | - | 文件上传地址 |
| `showSettings` | Boolean | true | 是否显示设置按钮 |
| `showTagsView` | Boolean | true | 是否显示标签栏 |
| `fixedHeader` | Boolean | true | 是否固定头部 |
| `sidebarLogo` | Boolean | true | 是否显示侧边栏 Logo |
| `cookieExpires` | Number | 1 | Token 在 Cookie 中存储的天数 |
| `themeColor` | String | '#2d8cf0' | 系统主题色 |
| `debug` | Boolean | false | 是否开启调试模式 |
---
## 🌍 环境配置
### 开发环境development
```javascript
{
apiUrl: 'http://localhost:9098/admin_api/',
uploadUrl: 'http://localhost:9098/admin_api/upload',
debug: true // 开发环境显示调试信息
}
```
**启动命令**
```bash
npm run serve
```
### 测试环境test
```javascript
{
apiUrl: 'http://test.yourdomain.com/admin_api/',
uploadUrl: 'http://test.yourdomain.com/admin_api/upload',
debug: false
}
```
**启动命令**
```bash
npm run build:test
```
### 生产环境production
```javascript
{
apiUrl: 'https://api.yourdomain.com/admin_api/',
uploadUrl: 'https://api.yourdomain.com/admin_api/upload',
debug: false
}
```
**启动命令**
```bash
npm run build
```
---
## 🔧 使用方法
### 在组件中使用配置
框架已将配置挂载到 `Vue.prototype.$config`,可以在任何组件中使用:
```vue
<template>
<div>
<h1>{{ $config.title }}</h1>
<p>API 地址: {{ $config.apiUrl }}</p>
</div>
</template>
<script>
export default {
mounted() {
console.log('系统标题:', this.$config.title)
console.log('API 地址:', this.$config.apiUrl)
console.log('是否调试模式:', this.$config.debug)
}
}
</script>
```
### 在 API 服务中使用配置
HTTP 工具已自动使用 `apiUrl` 作为基础路径:
```javascript
// 无需手动拼接 apiUrl框架会自动处理
this.$http.get('/user/list')
// 实际请求: http://localhost:9098/admin_api/user/list
```
---
## 📝 修改配置
### 修改基础配置
编辑 `config/index.js` 中的 `baseConfig`
```javascript
const baseConfig = {
title: '你的系统名称', // 修改系统标题
themeColor: '#409EFF', // 修改主题色
// ... 其他配置
}
```
### 修改环境配置
编辑对应环境的配置对象:
```javascript
// 开发环境配置
const developmentConfig = {
...baseConfig,
apiUrl: 'http://localhost:9098/admin_api/', // 修改开发环境 API 地址
uploadUrl: 'http://localhost:9098/admin_api/upload',
debug: true
}
// 生产环境配置
const productionConfig = {
...baseConfig,
apiUrl: 'https://api.yourdomain.com/admin_api/', // 修改生产环境 API 地址
uploadUrl: 'https://api.yourdomain.com/admin_api/upload',
debug: false
}
```
### 添加自定义配置
可以在任何环境配置中添加自定义字段:
```javascript
const developmentConfig = {
...baseConfig,
apiUrl: 'http://localhost:9098/admin_api/',
uploadUrl: 'http://localhost:9098/admin_api/upload',
// 自定义配置
enableMock: true,
websocketUrl: 'ws://localhost:9099',
maxFileSize: 10 * 1024 * 1024, // 10MB
allowedFileTypes: ['image/jpeg', 'image/png', 'application/pdf']
}
```
然后在组件中使用:
```javascript
if (this.$config.enableMock) {
console.log('启用 Mock 数据')
}
const ws = new WebSocket(this.$config.websocketUrl)
```
---
## 🚀 部署说明
### 开发环境部署
```bash
# 启动开发服务器
npm run serve
# 或使用 yarn
yarn serve
```
### 测试环境部署
```bash
# 构建测试环境代码
npm run build:test
# 将 dist 目录部署到测试服务器
```
### 生产环境部署
```bash
# 构建生产环境代码
npm run build
# 将 dist 目录部署到生产服务器
```
---
## ⚠️ 注意事项
1. **不要将敏感信息写入配置文件**
- API 密钥、数据库密码等敏感信息应通过环境变量传递
- 使用 `process.env.VUE_APP_*` 格式定义环境变量
2. **修改配置后需要重启**
- 修改配置文件后,需要重启开发服务器才能生效
- `Ctrl + C` 停止服务器,然后重新运行 `npm run serve`
3. **生产环境配置检查**
- 部署前务必检查生产环境的 `apiUrl` 是否正确
- 确保关闭 `debug` 模式
4. **跨域问题**
- 开发环境如遇到跨域问题,可以在 `vue.config.js` 中配置代理
- 生产环境需要后端配置 CORS
---
## 📚 相关文档
- [Vue CLI 环境变量和模式](https://cli.vuejs.org/zh/guide/mode-and-env.html)
- [AdminFramework 完整文档](../../_doc/admin_core完整使用文档.md)
---
**最后更新**: 2025-10-10

View File

@@ -0,0 +1,9 @@
/**
* 在业务组件中使用配置的示例(按需复制到 .vue 或独立模块)
*/
export const exampleComponent = {
mounted() {
console.log('title:', this.$config.title)
console.log('apiUrl:', this.$config.apiUrl)
}
}

57
admin/config/index.js Normal file
View File

@@ -0,0 +1,57 @@
/**
* Admin 前端配置(模板)
* 多环境development / sit / prod由 webpack DefinePlugin 注入 __APP_BUILD_ENV__
*/
const buildEnv = (typeof __APP_BUILD_ENV__ !== 'undefined' ? __APP_BUILD_ENV__ : (typeof process !== 'undefined' && process.env && process.env.BUILD_ENV)) || (typeof process !== 'undefined' && process.env && process.env.NODE_ENV) || 'development'
const baseConfig = {
title: '管理后台',
showSettings: true,
showTagsView: true,
fixedHeader: true,
sidebarLogo: true,
cookieExpires: 1,
themeColor: '#2d8cf0'
}
// 本地开发(默认)
const developmentConfig = {
...baseConfig,
apiUrl: 'http://localhost:9098/admin_api/',
uploadUrl: 'http://localhost:9098/admin_api/upload',
debug: true
}
// SIT 环境build:sit— 请在 .env.sit 中配合 BUILD_ENV=sit并按需改域名
const sitConfig = {
...baseConfig,
apiUrl: 'https://your-sit-domain.com/admin_api/',
uploadUrl: 'https://your-sit-domain.com/admin_api/upload',
debug: false
}
// 生产环境build:prod
const productionConfig = {
...baseConfig,
apiUrl: 'https://your-prod-domain.com/admin_api/',
uploadUrl: 'https://your-prod-domain.com/admin_api/upload',
debug: false
}
const configMap = {
development: developmentConfig,
sit: sitConfig,
production: productionConfig,
prod: productionConfig
}
const config = configMap[buildEnv] || developmentConfig
export default config
export {
baseConfig,
developmentConfig,
sitConfig,
productionConfig
}

7490
admin/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

39
admin/package.json Normal file
View File

@@ -0,0 +1,39 @@
{
"name": "admin-framework-demo",
"version": "1.0.0",
"description": "Admin 管理端模板(基于 Admin Framework",
"scripts": {
"install:deps": "npm install",
"dev": "webpack serve --mode development --open",
"build": "webpack --mode production",
"build:sit": "webpack --mode production --env env_file=.env.sit",
"build:prod": "webpack --mode production --env env_file=.env.prod",
"build:test": "webpack --mode test"
},
"dependencies": {
"axios": "^0.27.2",
"view-design": "^4.7.0",
"vue": "^2.6.14",
"vue-router": "^3.5.3",
"vuex": "^3.6.2"
},
"devDependencies": {
"@babel/core": "^7.12.0",
"@babel/preset-env": "^7.12.0",
"babel-loader": "^8.2.0",
"css-loader": "^5.0.0",
"file-loader": "^6.2.0",
"html-webpack-plugin": "^5.5.0",
"less": "^4.4.2",
"less-loader": "^12.3.0",
"style-loader": "^2.0.0",
"vue-loader": "^15.9.0",
"vue-style-loader": "^4.1.0",
"vue-template-compiler": "^2.6.14",
"webpack": "^5.0.0",
"webpack-cli": "^4.0.0",
"webpack-dev-server": "^4.0.0",
"cross-env": "^7.0.3",
"dotenv": "^16.0.3"
}
}

14
admin/public/index.html Normal file
View File

@@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>管理后台</title>
</head>
<body>
<div id="app"></div>
</body>
</html>

View File

@@ -0,0 +1,21 @@
class SysAdServer {
async getAll(param) {
return await window.framework.http.get("/sys_ad/index", param);
}
async add(row) {
return await window.framework.http.post("/sys_ad/add", row);
}
async edit(row) {
return await window.framework.http.post("/sys_ad/edit", row);
}
async del(row) {
return await window.framework.http.post("/sys_ad/del", row);
}
}
const sysAdServer = new SysAdServer();
export default sysAdServer;

View File

@@ -0,0 +1,11 @@
class BizPaymentServer {
async confirmOffline(row) {
return window.framework.http.post("/biz_payment/confirm-offline", row);
}
async confirmLink(row) {
return window.framework.http.post("/biz_payment/confirm-link", row);
}
}
export default new BizPaymentServer();

View File

@@ -0,0 +1,27 @@
class BizPlanServer {
async page(row) {
return window.framework.http.post("/biz_plan/page", row);
}
async add(row) {
return window.framework.http.post("/biz_plan/add", row);
}
async edit(row) {
return window.framework.http.post("/biz_plan/edit", row);
}
async del(row) {
return window.framework.http.post("/biz_plan/del", row);
}
async toggle(row) {
return window.framework.http.post("/biz_plan/toggle", row);
}
async all() {
return window.framework.http.get("/biz_plan/all", {});
}
}
export default new BizPlanServer();

View File

@@ -0,0 +1,27 @@
class BizSubscriptionServer {
async page(row) {
return window.framework.http.post("/biz_subscription/page", row);
}
async byUser(userId) {
return window.framework.http.get("/biz_subscription/by_user", { user_id: userId });
}
async open(row) {
return window.framework.http.post("/biz_subscription/open", row);
}
async upgrade(row) {
return window.framework.http.post("/biz_subscription/upgrade", row);
}
async renew(row) {
return window.framework.http.post("/biz_subscription/renew", row);
}
async cancel(row) {
return window.framework.http.post("/biz_subscription/cancel", row);
}
}
export default new BizSubscriptionServer();

View File

@@ -0,0 +1,15 @@
class BizTokenServer {
async page(row) {
return window.framework.http.post("/biz_token/page", row);
}
async create(row) {
return window.framework.http.post("/biz_token/create", row);
}
async revoke(row) {
return window.framework.http.post("/biz_token/revoke", row);
}
}
export default new BizTokenServer();

View File

@@ -0,0 +1,27 @@
class BizUserServer {
async page(row) {
return window.framework.http.post("/biz_user/page", row);
}
async add(row) {
return window.framework.http.post("/biz_user/add", row);
}
async edit(row) {
return window.framework.http.post("/biz_user/edit", row);
}
async del(row) {
return window.framework.http.post("/biz_user/del", row);
}
async detail(id) {
return window.framework.http.get(`/biz_user/detail?id=${encodeURIComponent(id)}`, {});
}
async disable(row) {
return window.framework.http.post("/biz_user/disable", row);
}
}
export default new BizUserServer();

View File

@@ -0,0 +1,33 @@
import request from '@/libs/http'
export const getList = (params) => {
return request({
url: '/sys_file/page',
method: 'get',
params
})
}
export const add = (data) => {
return request({
url: '/sys_file',
method: 'post',
data
})
}
export const edit = (data) => {
return request({
url: '/sys_file',
method: 'put',
data
})
}
export const del = (params) => {
return request({
url: '/sys_file',
method: 'delete',
params
})
}

View File

@@ -0,0 +1,36 @@
import config from '../../../config/index.js'
/**
* 使用原生 fetch 发送 multipart/form-data不设置 Content-Type由浏览器自动带 boundary
* 确保后端从 ctx.request.files 接收文件
*/
async function uploadOosImgWithFormData(formData) {
const apiUrl = (config.apiUrl || '').replace(/\/$/, '')
const url = `${apiUrl}/sys_file/upload_oos_img`
const headers = {}
if (window.framework && typeof window.framework.getToken === 'function') {
const token = window.framework.getToken()
if (token) headers['Authorization'] = token
}
const response = await fetch(url, { method: 'POST', headers, body: formData })
const data = await response.json().catch(() => ({}))
return data
}
class FileServe {
async upload_oos_img(row) {
if (row instanceof FormData) {
return uploadOosImgWithFormData(row)
}
let res = await window.framework.http.postFormData('/sys_file/upload_oos_img', row)
return res
}
async upload_Img(row) {
let res = await window.framework.http.postFormData("/file/upload_Img", row);
return res;
}
}
const fileServe = new FileServe();
export default fileServe;

View File

@@ -0,0 +1,30 @@
class RolePermissionServer {
async getRoles(callback) {
let res = await window.framework.http.get('/SysRolePermission/Query', {})
return res
}
async getRole(row) {
let res = await window.framework.http.get('/SysRolePermission/QueryByRoleId', row)
return res
}
async add(row) {
let res = await window.framework.http.post('/SysRolePermission/add', row)
return res
}
async edit(row) {
let res = await window.framework.http.post('/SysRolePermission/edit', row)
return res
}
async del(row) {
let res = await window.framework.http.post('/SysRolePermission/del', row)
return res
}
}
const rolePermissionServer = new RolePermissionServer()
export default rolePermissionServer

View File

@@ -0,0 +1,26 @@
class RoleServer {
async list() {
let res = await window.framework.http.get("/sys_role/index", {});
return res;
}
async add(row) {
let res = await window.framework.http.post("/sys_role/add", row);
return res;
}
async edit(row) {
let res = await window.framework.http.post("/sys_role/edit", row);
return res;
}
async del(row) {
let res = await window.framework.http.post("/sys_role/del", row);
return res;
}
}
const roleServer = new RoleServer();
export default roleServer;

View File

@@ -0,0 +1,10 @@
class SysAddress {
async index(param) {
let res = await window.framework.http.get("/sys_address/index", param);
return res;
}
}
const sysAddress = new SysAddress();
export default sysAddress;

View File

@@ -0,0 +1,30 @@
class SysModuleServer {
async all() {
let res = await window.framework.http.get("/sys_menu/all", {});
return res;
}
async list(row) {
let res = await window.framework.http.get("/sys_menu/all", row);
return res;
}
async add(row) {
let res = await window.framework.http.post("/sys_menu/add", row);
return res;
}
async edit(row) {
let res = await window.framework.http.post("/sys_menu/edit", row);
return res;
}
async del(row) {
let res = await window.framework.http.post("/sys_menu/del", row);
return res;
}
}
const sysModuleServer = new SysModuleServer();
export default sysModuleServer;

View File

@@ -0,0 +1,30 @@
class SysLogServe {
async all(param) {
let res = await window.framework.http.get("/sys_log/all", param);
return res;
}
async detail(param) {
let res = await window.framework.http.get("/sys_log/detail", param);
return res;
}
async delete(param) {
let res = await window.framework.http.get("/sys_log/delete", param);
return res;
}
async delete_all(param) {
let res = await window.framework.http.get("/sys_log/delete_all", param);
return res;
}
async operates(param) {
let res = await window.framework.http.get("/sys_log/operates", param);
return res;
}
}
const sys_log_serve = new SysLogServe();
export default sys_log_serve;

View File

@@ -0,0 +1,38 @@
class systemTypeClServer {
async all(param) {
let res= await window.framework.http.get('/sys_project_type/all', param);
return res;
}
async page(row) {
let res= await window.framework.http.post('/sys_project_type/page', row);
return res;
}
async exportCsv(row) {
let res = window.framework.http.fileExport("/sys_project_type/export", row);
return res;
}
async add(row) {
let res= await window.framework.http.post('/sys_project_type/add', row);
return res;
}
async edit(row) {
let res= await window.framework.http.post('/sys_project_type/edit', row);
return res;
}
async del(row) {
let res= await window.framework.http.post('/sys_project_type/del', row);
return res;
}
}
const systemTypeServer = new systemTypeClServer();
export default systemTypeServer;

View File

@@ -0,0 +1,31 @@
class TableServer {
async getAll(callback) {
return await window.framework.http.get('/table/index', {})
}
async add(row, callback) {
return await window.framework.http.post('/table/add', row)
}
async edit(row, callback) {
return await window.framework.http.post('/table/edit', row, function(res) {
callback && callback(res)
})
}
async del(row, callback) {
return await window.framework.http.post('/table/del', row)
}
async autoApi(id) {
return await window.framework.http.get('/template/api', { id: id })
}
async autoDb(id) {
return await window.framework.http.get('/template/autoDb', { id: id })
}
}
const tableServer = new TableServer()
export default tableServer

View File

@@ -0,0 +1,40 @@
class UserServer {
async login(row) {
let res = await window.framework.http.post("/sys_user/login", row);
return res;
}
async all() {
let res = await window.framework.http.get("/sys_user/index", {});
return res;
}
async exportCsv(row) {
let res = window.framework.http.fileExport("/sys_user/export", row);
return res;
}
async authorityMenus() {
let res = await window.framework.http.post("/sys_user/authorityMenus", {});
return res;
}
async add(row) {
let res = await window.framework.http.post("/sys_user/add", row);
return res;
}
async edit(row) {
let res = await window.framework.http.post("/sys_user/edit", row);
return res;
}
async del(row) {
let res = await window.framework.http.post("/sys_user/del", row);
return res;
}
}
const userServer = new UserServer();
export default userServer;

View File

@@ -0,0 +1,66 @@
<template>
<div v-show="isActive" class="tab-pane">
<slot></slot>
</div>
</template>
<script>
export default {
name: 'CustomTabPane',
props: {
label: {
type: String,
required: true
},
name: {
type: String,
required: true
}
},
data() {
return {
isActive: false,
parentTabs: null
}
},
mounted() {
this.findParentTabs()
if (this.parentTabs) {
this.parentTabs.registerTab({
label: this.label,
name: this.name
})
this.updateActiveState(this.parentTabs.activeTab)
}
},
beforeDestroy() {
if (this.parentTabs) {
this.parentTabs.unregisterTab(this.name)
}
},
methods: {
findParentTabs() {
let parent = this.$parent
while (parent) {
if (parent.$options.name === 'CustomTabs') {
this.parentTabs = parent
break
}
parent = parent.$parent
}
},
updateActiveState(activeTab) {
this.isActive = activeTab === this.name
}
}
}
</script>
<style scoped>
.tab-pane {
width: 100%;
overflow-x: auto;
overflow-y: visible;
}
</style>

View File

@@ -0,0 +1,118 @@
<template>
<div class="custom-tabs">
<div class="tabs-header">
<div
v-for="tab in tabs"
:key="tab.name"
:class="['tab-item', { active: activeTab === tab.name }]"
@click="handleTabClick(tab.name)">
{{ tab.label }}
</div>
</div>
<div class="tabs-content">
<slot></slot>
</div>
</div>
</template>
<script>
export default {
name: 'CustomTabs',
props: {
value: {
type: String,
default: ''
}
},
data() {
return {
activeTab: this.value || '',
tabs: []
}
},
watch: {
value(newVal) {
this.activeTab = newVal
this.updateChildren()
},
activeTab(newVal) {
this.updateChildren()
}
},
mounted() {
this.updateChildren()
},
methods: {
handleTabClick(name) {
this.activeTab = name
this.$emit('input', name)
this.$emit('change', name)
this.updateChildren()
},
registerTab(tab) {
if (!this.tabs.find(t => t.name === tab.name)) {
this.tabs.push(tab)
// 如果没有激活的tab设置第一个为激活
if (!this.activeTab && this.tabs.length > 0) {
this.activeTab = this.tabs[0].name
this.$emit('input', this.activeTab)
}
}
},
unregisterTab(tabName) {
const index = this.tabs.findIndex(t => t.name === tabName)
if (index > -1) {
this.tabs.splice(index, 1)
}
},
updateChildren() {
this.$children.forEach(child => {
if (child.$options.name === 'CustomTabPane') {
child.updateActiveState(this.activeTab)
}
})
}
}
}
</script>
<style scoped>
.custom-tabs {
width: 100%;
}
.tabs-header {
display: flex;
border-bottom: 1px solid #e8eaec;
background: #fff;
}
.tab-item {
padding: 12px 24px;
cursor: pointer;
color: #666;
font-size: 14px;
border-bottom: 2px solid transparent;
transition: all 0.3s;
user-select: none;
}
.tab-item:hover {
color: #333;
}
.tab-item.active {
color: #333;
border-bottom-color: #333;
font-weight: 500;
}
.tabs-content {
padding: 20px 0;
min-height: 400px;
overflow-x: auto;
overflow-y: visible;
width: 100%;
}
</style>

File diff suppressed because one or more lines are too long

39
admin/src/main.js Normal file
View File

@@ -0,0 +1,39 @@
// 引入 Admin Framework框架内部已包含所有依赖和样式
import AdminFramework from './framework/admin-framework.js'
import Vue from 'vue'
// 引入组件映射表
import componentMap from './router/component-map.js'
// 引入全局组件
import CustomTabs from './components/CustomTabs.vue'
import CustomTabPane from './components/CustomTabPane.vue'
import config from '../config/index.js'
const apiUrl = config.apiUrl
// 【超级简化】只需一个函数调用!
const app = AdminFramework.createApp({
title: '管理后台',
apiUrl: apiUrl, // API 地址uploadUrl 会自动设置为 apiUrl + 'upload'
componentMap: componentMap, // 传入组件映射表,用于动态路由
onReady() {
// 可选:应用启动完成后的回调
console.log('应用已准备就绪!')
}
})
// 注册全局组件
AdminFramework.registerComponents(Vue, {
'CustomTabs': CustomTabs,
'CustomTabPane': CustomTabPane
})
// 挂载应用
app.$mount('#app')

View File

@@ -0,0 +1,18 @@
// 组件映射表:后端菜单返回的 component 路径需与此处 key 一致(不含 .vue
import TestPage from '../views/test/test.vue'
import BizUsers from '../views/biz/biz_users.vue'
import BizPlans from '../views/biz/biz_plans.vue'
import BizSubscriptions from '../views/biz/biz_subscriptions.vue'
import BizTokens from '../views/biz/biz_tokens.vue'
import BizPayment from '../views/biz/biz_payment.vue'
const componentMap = {
'test/test': TestPage,
'biz/user': BizUsers,
'biz/plan': BizPlans,
'biz/subscription': BizSubscriptions,
'biz/token': BizTokens,
'biz/payment': BizPayment,
}
export default componentMap;

View File

@@ -0,0 +1,96 @@
<template>
<div class="biz-page">
<h2 class="biz-title">支付确认轻量</h2>
<p class="biz-desc"> <code>pending</code> 订阅置为 <code>active</code>并写入支付单号</p>
<Card dis-hover title="线下确认" style="max-width: 520px; margin-bottom: 16px">
<Form :label-width="110">
<FormItem label="订阅ID">
<Input v-model="offline.subscription_id" type="number" placeholder="biz_subscriptions.id" />
</FormItem>
<FormItem label="支付单号">
<Input v-model="offline.payment_ref" placeholder="流水号/凭证号" />
</FormItem>
<FormItem>
<Button type="primary" :loading="loading1" @click="doOffline">确认线下收款</Button>
</FormItem>
</Form>
</Card>
<Card dis-hover title="链接支付确认" style="max-width: 520px">
<Form :label-width="110">
<FormItem label="订阅ID">
<Input v-model="link.subscription_id" type="number" />
</FormItem>
<FormItem label="第三方单号">
<Input v-model="link.payment_ref" />
</FormItem>
<FormItem>
<Button type="primary" :loading="loading2" @click="doLink">确认链接支付</Button>
</FormItem>
</Form>
</Card>
</div>
</template>
<script>
import bizPaymentServer from '@/api/biz/biz_payment_server.js'
export default {
name: 'BizPayment',
data() {
return {
offline: { subscription_id: '', payment_ref: '' },
link: { subscription_id: '', payment_ref: '' },
loading1: false,
loading2: false,
}
},
methods: {
async doOffline() {
this.loading1 = true
try {
const res = await bizPaymentServer.confirmOffline({
subscription_id: Number(this.offline.subscription_id),
payment_ref: this.offline.payment_ref,
})
if (res && res.code === 0) {
this.$Message.success('已确认,订阅已激活')
} else {
this.$Message.error((res && res.message) || '失败')
}
} finally {
this.loading1 = false
}
},
async doLink() {
this.loading2 = true
try {
const res = await bizPaymentServer.confirmLink({
subscription_id: Number(this.link.subscription_id),
payment_ref: this.link.payment_ref,
})
if (res && res.code === 0) {
this.$Message.success('已确认,订阅已激活')
} else {
this.$Message.error((res && res.message) || '失败')
}
} finally {
this.loading2 = false
}
},
},
}
</script>
<style scoped>
.biz-page {
padding: 16px;
}
.biz-title {
margin: 0 0 8px;
font-size: 18px;
}
.biz-desc {
color: #666;
margin-bottom: 16px;
}
</style>

View File

@@ -0,0 +1,258 @@
<template>
<div class="biz-page">
<div class="biz-toolbar">
<h2 class="biz-title">套餐</h2>
<Button type="primary" @click="openEdit(null)">新增套餐</Button>
<Button class="ml8" @click="load(1)">刷新</Button>
</div>
<div class="biz-search">
<Form inline :label-width="70">
<FormItem label="条件">
<Select v-model="param.seachOption.key" style="width: 140px">
<Option value="plan_code">编码</Option>
<Option value="plan_name">名称</Option>
<Option value="status">状态</Option>
</Select>
<Input v-model="param.seachOption.value" class="ml8" style="width: 220px" search @on-search="load(1)" />
</FormItem>
<Button type="primary" @click="load(1)">查询</Button>
</Form>
</div>
<Table :columns="columns" :data="rows" border stripe />
<div class="biz-page-bar">
<Page
:total="total"
:current="param.pageOption.page"
:page-size="param.pageOption.pageSize"
show-total
@on-change="onPage"
@on-page-size-change="onSize"
/>
</div>
<Modal v-model="modal" :title="form.id ? '编辑套餐' : '新增套餐'" width="720" :loading="saving" @on-ok="save">
<Form ref="formRef" :model="form" :rules="rules" :label-width="120">
<FormItem label="套餐编码" prop="plan_code">
<Input v-model="form.plan_code" :disabled="!!form.id" />
</FormItem>
<FormItem label="套餐名称" prop="plan_name">
<Input v-model="form.plan_name" />
</FormItem>
<FormItem label="月费">
<Input v-model="form.monthly_price" type="number" />
</FormItem>
<FormItem label="授权费">
<Input v-model="form.auth_fee" type="number" />
</FormItem>
<FormItem label="账号上限">
<Input v-model="form.account_limit" type="number" />
</FormItem>
<FormItem label="活跃上限">
<Input v-model="form.active_user_limit" type="number" />
</FormItem>
<FormItem label="消息额度">
<Input v-model="form.msg_quota" type="number" />
</FormItem>
<FormItem label="群发额度">
<Input v-model="form.mass_quota" type="number" />
</FormItem>
<FormItem label="好友额度">
<Input v-model="form.friend_quota" type="number" />
</FormItem>
<FormItem label="朋友圈额度">
<Input v-model="form.sns_quota" type="number" />
</FormItem>
<FormItem label="功能点 JSON">
<Input v-model="featuresText" type="textarea" :rows="4" placeholder='如 {"msg":true} 或 ["msg","mass"]' />
</FormItem>
<FormItem label="状态" prop="status">
<Select v-model="form.status" style="width: 100%">
<Option value="active">上线</Option>
<Option value="inactive">下线</Option>
</Select>
</FormItem>
</Form>
</Modal>
</div>
</template>
<script>
import bizPlanServer from '@/api/biz/biz_plan_server.js'
export default {
name: 'BizPlans',
data() {
return {
rows: [],
total: 0,
param: {
seachOption: { key: 'plan_code', value: '' },
pageOption: { page: 1, pageSize: 20, total: 0 },
},
modal: false,
saving: false,
form: {},
featuresText: '{}',
rules: {
plan_code: [{ required: true, message: '必填', trigger: 'blur' }],
plan_name: [{ required: true, message: '必填', trigger: 'blur' }],
status: [{ required: true, message: '必选', trigger: 'change' }],
},
}
},
computed: {
columns() {
return [
{ title: 'ID', key: 'id', width: 70 },
{ title: '编码', key: 'plan_code', width: 120 },
{ title: '名称', key: 'plan_name', minWidth: 140 },
{ title: '月费', key: 'monthly_price', width: 90 },
{ title: '状态', key: 'status', width: 90 },
{
title: '操作',
key: 'a',
width: 260,
render: (h, p) =>
h('div', [
h('Button', { props: { type: 'info', size: 'small' }, on: { click: () => this.openEdit(p.row) } }, '编辑'),
h('Button', { props: { type: 'warning', size: 'small' }, class: { ml8: true }, on: { click: () => this.toggle(p.row) } }, '上下线'),
h('Button', { props: { type: 'error', size: 'small' }, class: { ml8: true }, on: { click: () => this.doDel(p.row) } }, '删除'),
]),
},
]
},
},
mounted() {
this.load(1)
},
methods: {
async load(page) {
if (page) this.param.pageOption.page = page
const res = await bizPlanServer.page({ param: this.param })
if (res && res.code === 0) {
this.rows = res.data.rows || []
this.total = res.data.count || 0
} else {
this.$Message.error((res && res.message) || '加载失败')
}
},
onPage(p) {
this.param.pageOption.page = p
this.load()
},
onSize(s) {
this.param.pageOption.pageSize = s
this.load(1)
},
openEdit(row) {
if (row) {
this.form = { ...row }
this.featuresText =
row.enabled_features == null
? ''
: typeof row.enabled_features === 'string'
? row.enabled_features
: JSON.stringify(row.enabled_features, null, 2)
} else {
this.form = {
plan_code: '',
plan_name: '',
monthly_price: 0,
auth_fee: 0,
account_limit: 0,
active_user_limit: 0,
msg_quota: 0,
mass_quota: 0,
friend_quota: 0,
sns_quota: 0,
status: 'active',
}
this.featuresText = '{}'
}
this.modal = true
},
save() {
this.saving = true
this.$refs.formRef.validate(async (ok) => {
if (!ok) {
this.saving = false
return
}
let enabled_features = null
const t = (this.featuresText || '').trim()
if (t) {
try {
enabled_features = JSON.parse(t)
} catch (e) {
this.$Message.error('功能点 JSON 格式错误')
this.saving = false
return
}
}
const payload = { ...this.form, enabled_features }
try {
const res = this.form.id ? await bizPlanServer.edit(payload) : await bizPlanServer.add(payload)
if (res && res.code === 0) {
this.$Message.success('保存成功')
this.modal = false
this.load(1)
} else {
this.$Message.error((res && res.message) || '保存失败')
}
} finally {
this.saving = false
}
})
},
async toggle(row) {
const res = await bizPlanServer.toggle({ id: row.id })
if (res && res.code === 0) {
this.$Message.success('状态已更新为 ' + (res.data && res.data.status))
this.load()
} else {
this.$Message.error((res && res.message) || '失败')
}
},
doDel(row) {
this.$Modal.confirm({
title: '删除套餐',
content: '确认删除?若已被订阅引用可能失败。',
onOk: async () => {
const res = await bizPlanServer.del({ id: row.id })
if (res && res.code === 0) {
this.$Message.success('已删除')
this.load(1)
} else {
this.$Message.error((res && res.message) || '删除失败')
}
},
})
},
},
}
</script>
<style scoped>
.biz-page {
padding: 16px;
}
.biz-toolbar {
margin-bottom: 12px;
}
.biz-title {
display: inline-block;
margin: 0 16px 0 0;
font-size: 18px;
vertical-align: middle;
}
.ml8 {
margin-left: 8px;
}
.biz-search {
margin-bottom: 12px;
}
.biz-page-bar {
margin-top: 12px;
text-align: right;
}
</style>

View File

@@ -0,0 +1,280 @@
<template>
<div class="biz-page">
<div class="biz-toolbar">
<h2 class="biz-title">订阅</h2>
<Button type="primary" @click="openOpen">开通订阅</Button>
<Button class="ml8" @click="load(1)">刷新</Button>
</div>
<div class="biz-search">
<Form inline>
<FormItem label="用户ID">
<Input v-model="param.seachOption.value" style="width: 140px" placeholder="筛选 user_id" />
</FormItem>
<FormItem>
<Select v-model="param.seachOption.key" style="width: 120px">
<Option value="user_id">用户ID</Option>
<Option value="status">状态</Option>
</Select>
</FormItem>
<Button type="primary" @click="load(1)">查询</Button>
</Form>
</div>
<Table :columns="columns" :data="rows" border stripe />
<div class="biz-page-bar">
<Page
:total="total"
:current="param.pageOption.page"
:page-size="param.pageOption.pageSize"
show-total
@on-change="onPage"
@on-page-size-change="onSize"
/>
</div>
<Modal v-model="openModal" title="开通订阅" width="640" :loading="saving" @on-ok="submitOpen">
<Form :label-width="110">
<FormItem label="用户ID"><Input v-model="openForm.user_id" type="number" /></FormItem>
<FormItem label="套餐ID"><Input v-model="openForm.plan_id" type="number" /></FormItem>
<FormItem label="开始时间"><Input v-model="openForm.start_time" placeholder="2025-01-01 00:00:00" /></FormItem>
<FormItem label="结束时间"><Input v-model="openForm.end_time" placeholder="2025-12-31 23:59:59" /></FormItem>
<FormItem label="状态">
<Select v-model="openForm.status" style="width: 100%">
<Option value="pending">pending</Option>
<Option value="active">active</Option>
</Select>
</FormItem>
<FormItem label="续费方式">
<Select v-model="openForm.renew_mode" style="width: 100%">
<Option value="manual">manual</Option>
<Option value="auto">auto</Option>
</Select>
</FormItem>
<FormItem label="支付渠道">
<Select v-model="openForm.payment_channel" clearable style="width: 100%">
<Option value="offline">offline</Option>
<Option value="pay_link">pay_link</Option>
</Select>
</FormItem>
<FormItem label="支付单号"><Input v-model="openForm.payment_ref" /></FormItem>
</Form>
</Modal>
<Modal v-model="renewModal" title="续费" :loading="saving" @on-ok="submitRenew">
<Form :label-width="100">
<FormItem label="新结束时间"><Input v-model="renewForm.end_time" /></FormItem>
</Form>
</Modal>
<Modal v-model="upgradeModal" title="升级套餐" :loading="saving" @on-ok="submitUpgrade">
<Form :label-width="100">
<FormItem label="新套餐ID"><Input v-model="upgradeForm.new_plan_id" type="number" /></FormItem>
<FormItem label="开始"><Input v-model="upgradeForm.start_time" /></FormItem>
<FormItem label="结束"><Input v-model="upgradeForm.end_time" /></FormItem>
</Form>
</Modal>
</div>
</template>
<script>
import bizSubscriptionServer from '@/api/biz/biz_subscription_server.js'
export default {
name: 'BizSubscriptions',
data() {
return {
rows: [],
total: 0,
param: {
seachOption: { key: 'user_id', value: '' },
pageOption: { page: 1, pageSize: 20, total: 0 },
},
openModal: false,
renewModal: false,
upgradeModal: false,
saving: false,
currentRow: null,
openForm: {},
renewForm: { end_time: '' },
upgradeForm: { new_plan_id: '', start_time: '', end_time: '' },
}
},
computed: {
columns() {
return [
{ title: 'ID', key: 'id', width: 70 },
{ title: '用户', key: 'user_id', width: 90 },
{ title: '套餐', key: 'plan_id', width: 90 },
{ title: '状态', key: 'status', width: 100 },
{ title: '开始', key: 'start_time', minWidth: 150 },
{ title: '结束', key: 'end_time', minWidth: 150 },
{
title: '操作',
key: 'a',
width: 200,
render: (h, p) =>
h('div', [
h('Button', { props: { size: 'small' }, on: { click: () => this.openRenew(p.row) } }, '续费'),
h('Button', { props: { size: 'small' }, class: { ml8: true }, on: { click: () => this.openUpgrade(p.row) } }, '升级'),
h('Button', { props: { type: 'error', size: 'small' }, class: { ml8: true }, on: { click: () => this.doCancel(p.row) } }, '取消'),
]),
},
]
},
},
mounted() {
this.load(1)
},
methods: {
async load(page) {
if (page) this.param.pageOption.page = page
const res = await bizSubscriptionServer.page({ param: this.param })
if (res && res.code === 0) {
this.rows = res.data.rows || []
this.total = res.data.count || 0
} else {
this.$Message.error((res && res.message) || '加载失败')
}
},
onPage(p) {
this.param.pageOption.page = p
this.load()
},
onSize(s) {
this.param.pageOption.pageSize = s
this.load(1)
},
openOpen() {
const now = new Date()
const end = new Date(now)
end.setFullYear(end.getFullYear() + 1)
const fmt = (d) =>
`${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')} ${String(
d.getHours()
).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}:00`
this.openForm = {
user_id: '',
plan_id: '',
start_time: fmt(now),
end_time: fmt(end),
status: 'pending',
renew_mode: 'manual',
payment_channel: '',
payment_ref: '',
}
this.openModal = true
},
async submitOpen() {
this.saving = true
try {
const body = {
user_id: Number(this.openForm.user_id),
plan_id: Number(this.openForm.plan_id),
start_time: this.openForm.start_time,
end_time: this.openForm.end_time,
status: this.openForm.status,
renew_mode: this.openForm.renew_mode,
payment_channel: this.openForm.payment_channel || null,
payment_ref: this.openForm.payment_ref || null,
}
const res = await bizSubscriptionServer.open(body)
if (res && res.code === 0) {
this.$Message.success('已创建订阅')
this.openModal = false
this.load(1)
} else {
this.$Message.error((res && res.message) || '失败')
}
} finally {
this.saving = false
}
},
openRenew(row) {
this.currentRow = row
this.renewForm = { end_time: row.end_time || '' }
this.renewModal = true
},
async submitRenew() {
if (!this.currentRow) return
this.saving = true
try {
const res = await bizSubscriptionServer.renew({
subscription_id: this.currentRow.id,
end_time: this.renewForm.end_time,
})
if (res && res.code === 0) {
this.$Message.success('已续费')
this.renewModal = false
this.load(1)
} else {
this.$Message.error((res && res.message) || '失败')
}
} finally {
this.saving = false
}
},
openUpgrade(row) {
this.currentRow = row
this.upgradeForm = { new_plan_id: row.plan_id, start_time: '', end_time: '' }
this.upgradeModal = true
},
async submitUpgrade() {
if (!this.currentRow) return
this.saving = true
try {
const res = await bizSubscriptionServer.upgrade({
subscription_id: this.currentRow.id,
new_plan_id: Number(this.upgradeForm.new_plan_id),
start_time: this.upgradeForm.start_time || undefined,
end_time: this.upgradeForm.end_time || undefined,
})
if (res && res.code === 0) {
this.$Message.success('已升级')
this.upgradeModal = false
this.load(1)
} else {
this.$Message.error((res && res.message) || '失败')
}
} finally {
this.saving = false
}
},
doCancel(row) {
this.$Modal.confirm({
title: '取消订阅',
content: '确认取消?',
onOk: async () => {
const res = await bizSubscriptionServer.cancel({ subscription_id: row.id })
if (res && res.code === 0) {
this.$Message.success('已取消')
this.load(1)
} else {
this.$Message.error((res && res.message) || '失败')
}
},
})
},
},
}
</script>
<style scoped>
.biz-page {
padding: 16px;
}
.biz-toolbar {
margin-bottom: 12px;
}
.biz-title {
display: inline-block;
margin: 0 16px 0 0;
font-size: 18px;
vertical-align: middle;
}
.ml8 {
margin-left: 8px;
}
.biz-page-bar {
margin-top: 12px;
text-align: right;
}
</style>

View File

@@ -0,0 +1,193 @@
<template>
<div class="biz-page">
<div class="biz-toolbar">
<h2 class="biz-title">API Token</h2>
<Button type="primary" @click="openCreate">创建 Token</Button>
<Button class="ml8" @click="load(1)">刷新</Button>
</div>
<div class="biz-search">
<Form inline>
<FormItem label="用户ID">
<Input v-model="param.seachOption.value" style="width: 140px" />
</FormItem>
<FormItem>
<Select v-model="param.seachOption.key" style="width: 120px">
<Option value="user_id">用户ID</Option>
<Option value="status">状态</Option>
</Select>
</FormItem>
<Button type="primary" @click="load(1)">查询</Button>
</Form>
</div>
<Table :columns="columns" :data="rows" border stripe />
<div class="biz-page-bar">
<Page
:total="total"
:current="param.pageOption.page"
:page-size="param.pageOption.pageSize"
show-total
@on-change="onPage"
@on-page-size-change="onSize"
/>
</div>
<Modal v-model="createModal" title="创建 Token" width="560" :loading="saving" @on-ok="submitCreate">
<Form :label-width="100">
<FormItem label="用户ID"><Input v-model="createForm.user_id" type="number" /></FormItem>
<FormItem label="名称"><Input v-model="createForm.token_name" placeholder="default" /></FormItem>
<FormItem label="过期时间"><Input v-model="createForm.expire_at" placeholder="2026-12-31 23:59:59" /></FormItem>
</Form>
</Modal>
<Modal v-model="plainModal" title="请立即保存 Token 明文" width="560" :closable="false">
<Alert type="error">仅此一次展示关闭后无法再次查看明文</Alert>
<Input type="textarea" :rows="4" v-model="plainToken" readonly />
<div slot="footer">
<Button type="primary" @click="plainModal = false">已保存</Button>
</div>
</Modal>
</div>
</template>
<script>
import bizTokenServer from '@/api/biz/biz_token_server.js'
export default {
name: 'BizTokens',
data() {
return {
rows: [],
total: 0,
param: {
seachOption: { key: 'user_id', value: '' },
pageOption: { page: 1, pageSize: 20, total: 0 },
},
createModal: false,
plainModal: false,
plainToken: '',
saving: false,
createForm: {},
}
},
computed: {
columns() {
return [
{ title: 'ID', key: 'id', width: 70 },
{ title: '用户', key: 'user_id', width: 90 },
{ title: '套餐', key: 'plan_id', width: 90 },
{ title: '名称', key: 'token_name', width: 120 },
{ title: '状态', key: 'status', width: 90 },
{ title: '过期', key: 'expire_at', minWidth: 150 },
{ title: '最后使用', key: 'last_used_at', minWidth: 150 },
{
title: '操作',
key: 'a',
width: 100,
render: (h, p) =>
h(
'Button',
{
props: { type: 'error', size: 'small' },
on: {
click: () => this.doRevoke(p.row),
},
},
'吊销'
),
},
]
},
},
mounted() {
this.load(1)
},
methods: {
async load(page) {
if (page) this.param.pageOption.page = page
const res = await bizTokenServer.page({ param: this.param })
if (res && res.code === 0) {
this.rows = res.data.rows || []
this.total = res.data.count || 0
} else {
this.$Message.error((res && res.message) || '加载失败')
}
},
onPage(p) {
this.param.pageOption.page = p
this.load()
},
onSize(s) {
this.param.pageOption.pageSize = s
this.load(1)
},
openCreate() {
const d = new Date()
d.setFullYear(d.getFullYear() + 1)
const fmt = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(
2,
'0'
)} 23:59:59`
this.createForm = { user_id: '', token_name: 'default', expire_at: fmt }
this.createModal = true
},
async submitCreate() {
this.saving = true
try {
const res = await bizTokenServer.create({
user_id: Number(this.createForm.user_id),
token_name: this.createForm.token_name || 'default',
expire_at: this.createForm.expire_at,
})
if (res && res.code === 0) {
if (res.data.warn) this.$Message.warning(res.data.warn)
this.createModal = false
this.plainToken = res.data.plain_token
this.plainModal = true
this.load(1)
} else {
this.$Message.error((res && res.message) || '创建失败')
}
} finally {
this.saving = false
}
},
doRevoke(row) {
this.$Modal.confirm({
title: '吊销 Token',
content: '确认吊销?',
onOk: async () => {
const res = await bizTokenServer.revoke({ id: row.id })
if (res && res.code === 0) {
this.$Message.success('已吊销')
this.load(1)
} else {
this.$Message.error((res && res.message) || '失败')
}
},
})
},
},
}
</script>
<style scoped>
.biz-page {
padding: 16px;
}
.biz-toolbar {
margin-bottom: 12px;
}
.biz-title {
display: inline-block;
margin: 0 16px 0 0;
font-size: 18px;
vertical-align: middle;
}
.ml8 {
margin-left: 8px;
}
.biz-page-bar {
margin-top: 12px;
text-align: right;
}
</style>

View File

@@ -0,0 +1,270 @@
<template>
<div class="biz-page">
<div class="biz-toolbar">
<h2 class="biz-title">业务用户</h2>
<Button type="primary" @click="openEdit(null)">新增</Button>
<Button class="ml8" @click="load(1)">刷新</Button>
</div>
<div class="biz-search">
<Form inline :label-width="70">
<FormItem label="条件">
<Select v-model="param.seachOption.key" style="width: 140px">
<Option value="mobile">手机</Option>
<Option value="company_name">公司</Option>
<Option value="status">状态</Option>
</Select>
<Input v-model="param.seachOption.value" placeholder="关键字" style="width: 220px" class="ml8" search @on-search="load(1)" />
</FormItem>
<FormItem>
<Button type="primary" @click="load(1)">查询</Button>
</FormItem>
</Form>
</div>
<Table :columns="columns" :data="rows" border stripe />
<div class="biz-page-bar">
<Page
:total="total"
:current="param.pageOption.page"
:page-size="param.pageOption.pageSize"
show-total
@on-change="onPage"
@on-page-size-change="onSize"
/>
</div>
<Modal v-model="modal" :title="form.id ? '编辑用户' : '新增用户'" width="640" :loading="saving" @on-ok="save">
<Form ref="formRef" :model="form" :rules="rules" :label-width="100">
<FormItem label="名称" prop="name">
<Input v-model="form.name" />
</FormItem>
<FormItem label="手机" prop="mobile">
<Input v-model="form.mobile" />
</FormItem>
<FormItem label="邮箱">
<Input v-model="form.email" />
</FormItem>
<FormItem label="公司">
<Input v-model="form.company_name" />
</FormItem>
<FormItem label="状态" prop="status">
<Select v-model="form.status" style="width: 100%">
<Option value="active">正常</Option>
<Option value="disabled">禁用</Option>
</Select>
</FormItem>
</Form>
</Modal>
<Modal v-model="detailVisible" title="用户详情" width="720" footer-hide>
<p v-if="detail">Token 数量{{ detail.tokenCount }}</p>
<Table v-if="detail && detail.subscriptions" :columns="subCols" :data="detail.subscriptions" size="small" border />
</Modal>
</div>
</template>
<script>
import bizUserServer from '@/api/biz/biz_user_server.js'
export default {
name: 'BizUsers',
data() {
return {
rows: [],
total: 0,
param: {
seachOption: { key: 'mobile', value: '' },
pageOption: { page: 1, pageSize: 20, total: 0 },
},
modal: false,
saving: false,
form: {},
rules: {
name: [{ required: true, message: '请输入名称', trigger: 'blur' }],
status: [{ required: true, message: '请选择状态', trigger: 'change' }],
},
detailVisible: false,
detail: null,
subCols: [
{ title: 'ID', key: 'id', width: 80 },
{ title: '套餐ID', key: 'plan_id', width: 90 },
{ title: '状态', key: 'status', width: 100 },
{ title: '开始', key: 'start_time', minWidth: 160 },
{ title: '结束', key: 'end_time', minWidth: 160 },
],
}
},
computed: {
columns() {
return [
{ title: 'ID', key: 'id', width: 80 },
{ title: '名称', key: 'name', minWidth: 120 },
{ title: '手机', key: 'mobile', width: 130 },
{ title: '公司', key: 'company_name', minWidth: 140 },
{ title: '状态', key: 'status', width: 90 },
{
title: '操作',
key: 'a',
width: 220,
render: (h, p) => {
return h('div', [
h(
'Button',
{
props: { type: 'info', size: 'small' },
on: { click: () => this.openEdit(p.row) },
},
'编辑'
),
h(
'Button',
{
props: { type: 'default', size: 'small' },
class: { ml8: true },
on: { click: () => this.showDetail(p.row) },
},
'详情'
),
h(
'Button',
{
props: { type: 'warning', size: 'small' },
class: { ml8: true },
on: { click: () => this.doDisable(p.row) },
},
'禁用'
),
h(
'Button',
{
props: { type: 'error', size: 'small' },
class: { ml8: true },
on: { click: () => this.doDel(p.row) },
},
'删除'
),
])
},
},
]
},
},
mounted() {
this.load(1)
},
methods: {
async load(page) {
if (page) this.param.pageOption.page = page
const res = await bizUserServer.page({ param: this.param })
if (res && res.code === 0) {
this.rows = res.data.rows || []
this.total = res.data.count || 0
} else {
this.$Message.error((res && res.message) || '加载失败')
}
},
onPage(p) {
this.param.pageOption.page = p
this.load()
},
onSize(s) {
this.param.pageOption.pageSize = s
this.load(1)
},
openEdit(row) {
if (row) {
this.form = { ...row }
} else {
this.form = { name: '', mobile: '', email: '', company_name: '', status: 'active' }
}
this.modal = true
},
save() {
this.saving = true
this.$refs.formRef.validate(async (ok) => {
if (!ok) {
this.saving = false
return
}
try {
const res = this.form.id
? await bizUserServer.edit(this.form)
: await bizUserServer.add(this.form)
if (res && res.code === 0) {
this.$Message.success('保存成功')
this.modal = false
this.load(1)
} else {
this.$Message.error((res && res.message) || '保存失败')
}
} finally {
this.saving = false
}
})
},
async showDetail(row) {
const res = await bizUserServer.detail(row.id)
if (res && res.code === 0) {
this.detail = res.data
this.detailVisible = true
} else {
this.$Message.error((res && res.message) || '加载详情失败')
}
},
doDisable(row) {
this.$Modal.confirm({
title: '禁用用户',
content: '确认禁用该用户?',
onOk: async () => {
const res = await bizUserServer.disable({ id: row.id })
if (res && res.code === 0) {
this.$Message.success('已禁用')
this.load(1)
} else {
this.$Message.error((res && res.message) || '操作失败')
}
},
})
},
doDel(row) {
this.$Modal.confirm({
title: '删除用户',
content: '确认删除?若存在订阅/Token 可能受外键限制。',
onOk: async () => {
const res = await bizUserServer.del({ id: row.id })
if (res && res.code === 0) {
this.$Message.success('已删除')
this.load(1)
} else {
this.$Message.error((res && res.message) || '删除失败')
}
},
})
},
},
}
</script>
<style scoped>
.biz-page {
padding: 16px;
}
.biz-toolbar {
margin-bottom: 12px;
}
.biz-title {
display: inline-block;
margin: 0 16px 0 0;
font-size: 18px;
vertical-align: middle;
}
.ml8 {
margin-left: 8px;
}
.biz-search {
margin-bottom: 12px;
}
.biz-page-bar {
margin-top: 12px;
text-align: right;
}
</style>

View File

@@ -0,0 +1,35 @@
<template>
<div class="test-page">
<h2 class="test-page__title">Test</h2>
<p class="test-page__desc">基础测试页面动态菜单的 component 请填 <code>test/test</code></p>
</div>
</template>
<script>
export default {
name: 'TestPage'
}
</script>
<style scoped>
.test-page {
padding: 24px;
}
.test-page__title {
margin: 0 0 12px;
font-size: 18px;
font-weight: 600;
}
.test-page__desc {
margin: 0;
color: #666;
font-size: 14px;
line-height: 1.6;
}
.test-page__desc code {
padding: 2px 6px;
font-size: 13px;
background: #f5f5f5;
border-radius: 4px;
}
</style>

118
admin/webpack.config.js Normal file
View File

@@ -0,0 +1,118 @@
const path = require('path')
const webpack = require('webpack')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const { VueLoaderPlugin } = require('vue-loader')
module.exports = (env, argv) => {
// 仅当传入 env_file 时加载 .envbuild:sit / build:prod否则 dev 用 developmentbuild 用 prod
const envFile = env && env.env_file
if (envFile) {
require('dotenv').config({ path: path.resolve(__dirname, envFile) })
}
const buildEnv = process.env.BUILD_ENV || (argv.mode === 'production' ? 'prod' : 'development')
return {
entry: './src/main.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'js/[name].[contenthash:8].js',
chunkFilename: 'js/[name].[contenthash:8].chunk.js',
clean: true
},
optimization: {
splitChunks: {
chunks: 'all',
minSize: 20000,
maxSize: 244000,
cacheGroups: {
// Vue 相关库单独打包
vue: {
test: /[\\/]node_modules[\\/](vue|vue-router|vuex|vue-loader|vue-template-compiler)[\\/]/,
name: 'vue',
priority: 30,
reuseExistingChunk: true
},
// UI 库单独打包
ui: {
test: /[\\/]node_modules[\\/](view-design|iview)[\\/]/,
name: 'ui',
priority: 25,
reuseExistingChunk: true
},
// 其他第三方库打包
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
priority: 10,
reuseExistingChunk: true
},
// 公共代码
common: {
name: 'common',
minChunks: 2,
priority: 5,
reuseExistingChunk: true
}
}
},
// 运行时代码单独打包
runtimeChunk: {
name: 'runtime'
},
// 生产环境启用压缩
minimize: process.env.NODE_ENV === 'production'
},
module: {
rules: [
{
test: /\.vue$/,
loader: 'vue-loader'
},
{
test: /\.js$/,
loader: 'babel-loader',
exclude: /node_modules/
},
{
test: /\.css$/,
use: ['vue-style-loader', 'css-loader']
},
{
test: /\.less$/,
use: ['vue-style-loader', 'css-loader', 'less-loader']
},
{
test: /\.(png|jpe?g|gif|svg|woff2?|eot|ttf|otf)$/,
type: 'asset/resource',
generator: {
filename: 'assets/[name].[hash:8][ext]'
}
}
]
},
plugins: [
new webpack.DefinePlugin({
__APP_BUILD_ENV__: JSON.stringify(buildEnv),
'process.env.BUILD_ENV': JSON.stringify(buildEnv)
}),
new VueLoaderPlugin(),
new HtmlWebpackPlugin({
template: './public/index.html',
title: '管理后台'
})
],
resolve: {
extensions: ['.js', '.vue', '.json'],
alias: {
'@': path.resolve(__dirname, 'src'),
'vue$': 'vue/dist/vue.esm.js'
}
},
devServer: {
hot: true,
open: true,
port: 8080,
historyApiFallback: true
}
}
}

View File

@@ -0,0 +1,15 @@
const crud = require("../service/biz_admin_crud");
const { getRequestBody } = crud;
module.exports = {
"POST /biz_audit_log/page": async (ctx) => {
const body = getRequestBody(ctx);
const data = await crud.page("biz_audit_log", body);
ctx.success({ rows: data.rows, count: data.count });
},
"POST /biz_audit_log/export": async (ctx) => {
const body = getRequestBody(ctx);
const res = await crud.exportCsv("biz_audit_log", body);
ctx.success(res);
},
};

View File

@@ -0,0 +1,8 @@
const dashboard = require("../service/biz_dashboard_service");
module.exports = {
"GET /biz_dashboard/summary": async (ctx) => {
const data = await dashboard.summary();
ctx.success(data);
},
};

View File

@@ -0,0 +1,24 @@
const logic = require("../service/biz_subscription_logic");
function getRequestBody(ctx) {
if (ctx.request && ctx.request.body && Object.keys(ctx.request.body).length > 0) {
return ctx.request.body;
}
if (typeof ctx.getBody === "function") {
return ctx.getBody() || {};
}
return {};
}
module.exports = {
"POST /biz_payment/confirm-offline": async (ctx) => {
const body = getRequestBody(ctx);
const row = await logic.confirmOfflinePayment(body);
ctx.success(row);
},
"POST /biz_payment/confirm-link": async (ctx) => {
const body = getRequestBody(ctx);
const row = await logic.confirmLinkPayment(body);
ctx.success(row);
},
};

View File

@@ -0,0 +1,77 @@
const crud = require("../service/biz_admin_crud");
const { getRequestBody } = crud;
const baseModel = require("../../middleware/baseModel");
const audit = require("../service/biz_audit_service");
module.exports = {
"POST /biz_plan/page": async (ctx) => {
const body = getRequestBody(ctx);
const data = await crud.page("biz_plan", body);
ctx.success({ rows: data.rows, count: data.count });
},
"POST /biz_plan/add": async (ctx) => {
const body = getRequestBody(ctx);
const row = await crud.add("biz_plan", body);
await audit.logAudit({
admin_user_id: audit.pickAdminId(ctx),
action: "biz_plan.add",
resource_type: "biz_plan",
resource_id: row.id,
detail: { plan_code: row.plan_code },
});
ctx.success(row);
},
"POST /biz_plan/edit": async (ctx) => {
const body = getRequestBody(ctx);
await crud.edit("biz_plan", body);
await audit.logAudit({
admin_user_id: audit.pickAdminId(ctx),
action: "biz_plan.edit",
resource_type: "biz_plan",
resource_id: body.id,
});
ctx.success({});
},
"POST /biz_plan/del": async (ctx) => {
const body = getRequestBody(ctx);
await crud.del("biz_plan", body);
await audit.logAudit({
admin_user_id: audit.pickAdminId(ctx),
action: "biz_plan.del",
resource_type: "biz_plan",
resource_id: body.id,
});
ctx.success({});
},
"GET /biz_plan/detail": async (ctx) => {
const q = ctx.query || {};
const row = await crud.detail("biz_plan", { id: q.id || q.ID });
ctx.success(row);
},
"GET /biz_plan/all": async (ctx) => {
const rows = await crud.all("biz_plan");
ctx.success(rows);
},
"POST /biz_plan/toggle": async (ctx) => {
const body = getRequestBody(ctx);
const id = body.id;
if (id == null) return ctx.fail("缺少 id");
const row = await baseModel.biz_plan.findByPk(id);
if (!row) return ctx.fail("套餐不存在");
const next = row.status === "active" ? "inactive" : "active";
await row.update({ status: next });
await audit.logAudit({
admin_user_id: audit.pickAdminId(ctx),
action: "biz_plan.toggle",
resource_type: "biz_plan",
resource_id: id,
detail: { status: next },
});
ctx.success({ status: next });
},
"POST /biz_plan/export": async (ctx) => {
const body = getRequestBody(ctx);
const res = await crud.exportCsv("biz_plan", body);
ctx.success(res);
},
};

View File

@@ -0,0 +1,56 @@
const crud = require("../service/biz_admin_crud");
const baseModel = require("../../middleware/baseModel");
const logic = require("../service/biz_subscription_logic");
function getRequestBody(ctx) {
if (ctx.request && ctx.request.body && Object.keys(ctx.request.body).length > 0) {
return ctx.request.body;
}
if (typeof ctx.getBody === "function") {
return ctx.getBody() || {};
}
return {};
}
module.exports = {
"POST /biz_subscription/page": async (ctx) => {
const body = getRequestBody(ctx);
const data = await crud.page("biz_subscription", body);
ctx.success({ rows: data.rows, count: data.count });
},
"GET /biz_subscription/detail": async (ctx) => {
const q = ctx.query || {};
const row = await crud.detail("biz_subscription", { id: q.id || q.ID });
ctx.success(row);
},
"GET /biz_subscription/by_user": async (ctx) => {
const q = ctx.query || {};
const user_id = q.user_id || q.userId;
if (!user_id) return ctx.fail("缺少 user_id");
const rows = await baseModel.biz_subscription.findAll({
where: { user_id },
order: [["id", "DESC"]],
});
ctx.success(rows);
},
"POST /biz_subscription/open": async (ctx) => {
const body = getRequestBody(ctx);
const row = await logic.openSubscription(body);
ctx.success(row);
},
"POST /biz_subscription/upgrade": async (ctx) => {
const body = getRequestBody(ctx);
const row = await logic.upgradeSubscription(body);
ctx.success(row);
},
"POST /biz_subscription/renew": async (ctx) => {
const body = getRequestBody(ctx);
const row = await logic.renewSubscription(body);
ctx.success(row);
},
"POST /biz_subscription/cancel": async (ctx) => {
const body = getRequestBody(ctx);
const row = await logic.cancelSubscription(body);
ctx.success(row);
},
};

View File

@@ -0,0 +1,34 @@
const crud = require("../service/biz_admin_crud");
const { getRequestBody } = crud;
const tokenLogic = require("../service/biz_token_logic");
const audit = require("../service/biz_audit_service");
module.exports = {
"POST /biz_token/page": async (ctx) => {
const body = getRequestBody(ctx);
const data = await crud.page("biz_api_token", body);
ctx.success({ rows: data.rows, count: data.count });
},
"POST /biz_token/create": async (ctx) => {
const body = getRequestBody(ctx);
const result = await tokenLogic.createToken(body);
await audit.logAudit({
admin_user_id: audit.pickAdminId(ctx),
biz_user_id: result.row.user_id,
action: "biz_token.create",
resource_type: "biz_api_token",
resource_id: result.row.id,
detail: { token_name: result.row.token_name },
});
ctx.success({
id: result.row.id,
user_id: result.row.user_id,
plan_id: result.row.plan_id,
token_name: result.row.token_name,
expire_at: result.row.expire_at,
plain_token: result.plain_token,
warn: result.warn,
});
},
"POST /biz_token/revoke": async (ctx) => {
const body = getRequestBody(ct

View File

@@ -0,0 +1,35 @@
const crud = require("../service/biz_admin_crud");
const { getRequestBody } = crud;
module.exports = {
"POST /biz_usage/page": async (ctx) => {
const body = getRequestBody(ctx);
const data = await crud.page("biz_usage_monthly", body);
ctx.success({ rows: data.rows, count: data.count });
},
"POST /biz_usage/add": async (ctx) => {
const body = getRequestBody(ctx);
const row = await crud.add("biz_usage_monthly", body);
ctx.success(row);
},
"POST /biz_usage/edit": async (ctx) => {
const body = getRequestBody(ctx);
await crud.edit("biz_usage_monthly", body);
ctx.success({});
},
"POST /biz_usage/del": async (ctx) => {
const body = getRequestBody(ctx);
await crud.del("biz_usage_monthly", body);
ctx.success({});
},
"GET /biz_usage/detail": async (ctx) => {
const q = ctx.query || {};
const row = await crud.detail("biz_usage_monthly", { id: q.id || q.ID });
ctx.success(row);
},
"POST /biz_usage/export": async (ctx) => {
const body = getRequestBody(ctx);
const res = await crud.exportCsv("biz_usage_monthly", body);
ctx.success(res);
},
};

View File

@@ -0,0 +1,109 @@
const crud = require("../service/biz_admin_crud");
const { getRequestBody } = crud;
const baseModel = require("../../middleware/baseModel");
const tokenLogic = require("../service/biz_token_logic");
const audit = require("../service/biz_audit_service");
module.exports = {
"POST /biz_user/page": async (ctx) => {
const body = getRequestBody(ctx);
const data = await crud.page("biz_user", body);
ctx.success({ rows: data.rows, count: data.count });
},
"POST /biz_user/add": async (ctx) => {
const body = getRequestBody(ctx);
const row = await crud.add("biz_user", body);
await audit.logAudit({
admin_user_id: audit.pickAdminId(ctx),
biz_user_id: row.id,
action: "biz_user.add",
resource_type: "biz_user",
resource_id: row.id,
detail: { name: row.name },
});
ctx.success(row);
},
"POST /biz_user/edit": async (ctx) => {
const body = getRequestBody(ctx);
await crud.edit("biz_user", body);
await audit.logAudit({
admin_user_id: audit.pickAdminId(ctx),
biz_user_id: body.id,
action: "biz_user.edit",
resource_type: "biz_user",
resource_id: body.id,
});
ctx.success({});
},
"POST /biz_user/del": async (ctx) => {
const body = getRequestBody(ctx);
await crud.del("biz_user", body);
await audit.logAudit({
admin_user_id: audit.pickAdminId(ctx),
biz_user_id: body.id,
action: "biz_user.del",
resource_type: "biz_user",
resource_id: body.id,
});
ctx.success({});
},
"GET /biz_user/detail": async (ctx) => {
const q = ctx.query || {};
const id = q.id || q.ID;
const user = await crud.detail("biz_user", { id });
if (!user) {
return ctx.fail("用户不存在");
}
const subscriptions = await baseModel.biz_subscription.findAll({
where: { user_id: id },
order: [["id", "DESC"]],
limit: 10,
});
const tokenCount = await baseModel.biz_api_token.count({
where: { user_id: id },
});
ctx.success({
user,
subscriptions,
tokenCount,
});
},
"GET /biz_user/all": async (ctx) => {
const rows = await crud.all("biz_user");
ctx.success(rows);
},
"POST /biz_user/disable": async (ctx) => {
const body = getRequestBody(ctx);
const id = body.id;
if (id == null) return ctx.fail("缺少 id");
await baseModel.biz_user.update({ status: "disabled" }, { where: { id } });
await audit.logAudit({
admin_user_id: audit.pickAdminId(ctx),
biz_user_id: id,
action: "biz_user.disable",
resource_type: "biz_user",
resource_id: id,
});
ctx.success({});
},
"POST /biz_user/export": async (ctx) => {
const body = getRequestBody(ctx);
const res = await crud.exportCsv("biz_user", body);
ctx.success(res);
},
"POST /biz_user/revoke_all_tokens": async (ctx) => {
const body = getRequestBody(ctx);
const userId = body.user_id != null ? body.user_id : body.id;
if (userId == null) return ctx.fail("缺少 user_id");
const n = await tokenLogic.revokeAllForUser(userId);
await audit.logAudit({
admin_user_id: audit.pickAdminId(ctx),
biz_user_id: userId,
action: "biz_token.revoke_all",
resource_type: "biz_user",
resource_id: userId,
detail: { affected: n },
});
ctx.success({ revoked: n });
},
};

View File

@@ -0,0 +1,49 @@
var UUID = require("uuid");
var fs = require("fs");
var path = require("path");
const ossTool = require("../service/ossTool");
const funTool = require("../../tool/funTool");
module.exports = {
"POST /sys_file/upload_img": async (ctx, next) => {
const files = ctx.request.files; // 获取上传文件
let fileArray = [];
let rootPath = path.join(__dirname, "../../upload/imgs");
for (var key in files) {
fileArray.push(files[key]);
}
//创建文件夹
await funTool.mkdirsSync(rootPath);
let resArray = [];
fileArray.forEach((file) => {
// 创建可读流
const reader = fs.createReadStream(file.path);
let filePath = `/${UUID.v1() + "_" + file.name}`;
// 创建可写流
const upStream = fs.createWriteStream(path.join(rootPath, filePath));
// 可读流通过管道写入可写流
reader.pipe(upStream);
resArray.push({ name: file.name, path: path.join("/imgs", filePath) });
});
ctx.success(resArray);
},
"POST /sys_file/upload_oos_img": async (ctx, next) => {
let fileArray = [];
const files = ctx.request.files; // 获取上传文件
for (var key in files) {
fileArray.push(files[key]);
}
let data = await ossTool.putImg(fileArray[0]);
if (data.path) {
return ctx.success(data);
} else {
return ctx.fail();
}
},
};

View File

@@ -0,0 +1,19 @@
const auth = require("../service/biz_auth_verify");
function getRequestBody(ctx) {
if (ctx.request && ctx.request.body && Object.keys(ctx.request.body).length > 0) {
return ctx.request.body;
}
if (typeof ctx.getBody === "function") {
return ctx.getBody() || {};
}
return {};
}
module.exports = {
"POST /auth/verify": async (ctx) => {
const body = getRequestBody(ctx);
const result = await auth.verifyRequest(body);
ctx.success(result);
},
};

View File

View File

@@ -0,0 +1,46 @@
const Sequelize = require("sequelize");
module.exports = (db) => {
return db.define(
"biz_api_token",
{
id: {
type: Sequelize.BIGINT.UNSIGNED,
primaryKey: true,
autoIncrement: true,
},
user_id: {
type: Sequelize.BIGINT.UNSIGNED,
allowNull: false,
},
plan_id: {
type: Sequelize.BIGINT.UNSIGNED,
allowNull: true,
comment: "冗余:鉴权时少联表",
},
token_name: {
type: Sequelize.STRING(100),
allowNull: false,
defaultValue: "",
},
token_hash: {
type: Sequelize.STRING(64),
allowNull: false,
unique: true,
},
status: {
type: Sequelize.ENUM("active", "revoked", "expired"),
allowNull: false,
defaultValue: "active",
},
expire_at: { type: Sequelize.DATE, allowNull: false },
last_used_at: { type: Sequelize.DATE, allowNull: true },
},
{
tableName: "biz_api_tokens",
timestamps: true,
underscored: true,
comment: "API Token",
}
);
};

View File

@@ -0,0 +1,54 @@
const Sequelize = require("sequelize");
module.exports = (db) => {
return db.define(
"biz_audit_log",
{
id: {
type: Sequelize.BIGINT.UNSIGNED,
primaryKey: true,
autoIncrement: true,
},
admin_user_id: {
type: Sequelize.BIGINT.UNSIGNED,
allowNull: true,
},
biz_user_id: {
type: Sequelize.BIGINT.UNSIGNED,
allowNull: true,
},
action: {
type: Sequelize.STRING(64),
allowNull: false,
},
resource_type: {
type: Sequelize.STRING(64),
allowNull: false,
defaultValue: "",
},
resource_id: {
type: Sequelize.BIGINT.UNSIGNED,
allowNull: true,
},
detail: {
type: Sequelize.JSON,
allowNull: true,
},
created_at: {
type: Sequelize.DATE,
allowNull: false,
},
},
{
tableName: "biz_audit_log",
timestamps: false,
underscored: true,
comment: "审计日志",
hooks: {
beforeCreate(row) {
if (!row.created_at) row.created_at = new Date();
},
},
}
);
};

56
api/model/biz_plan.js Normal file
View File

@@ -0,0 +1,56 @@
const Sequelize = require("sequelize");
module.exports = (db) => {
return db.define(
"biz_plan",
{
id: {
type: Sequelize.BIGINT.UNSIGNED,
primaryKey: true,
autoIncrement: true,
},
plan_code: {
type: Sequelize.STRING(64),
allowNull: false,
unique: true,
},
plan_name: {
type: Sequelize.STRING(128),
allowNull: false,
defaultValue: "",
},
monthly_price: {
type: Sequelize.DECIMAL(12, 2),
allowNull: false,
defaultValue: 0,
},
auth_fee: {
type: Sequelize.DECIMAL(12, 2),
allowNull: false,
defaultValue: 0,
},
account_limit: { type: Sequelize.INTEGER, allowNull: false, defaultValue: 0 },
active_user_limit: { type: Sequelize.INTEGER, allowNull: false, defaultValue: 0 },
msg_quota: { type: Sequelize.INTEGER, allowNull: false, defaultValue: 0 },
mass_quota: { type: Sequelize.INTEGER, allowNull: false, defaultValue: 0 },
friend_quota: { type: Sequelize.INTEGER, allowNull: false, defaultValue: 0 },
sns_quota: { type: Sequelize.INTEGER, allowNull: false, defaultValue: 0 },
enabled_features: {
type: Sequelize.JSON,
allowNull: true,
comment: "JSON 功能点白名单",
},
status: {
type: Sequelize.ENUM("active", "inactive"),
allowNull: false,
defaultValue: "active",
},
},
{
tableName: "biz_plans",
timestamps: true,
underscored: true,
comment: "套餐",
}
);
};

View File

@@ -0,0 +1,48 @@
const Sequelize = require("sequelize");
module.exports = (db) => {
return db.define(
"biz_subscription",
{
id: {
type: Sequelize.BIGINT.UNSIGNED,
primaryKey: true,
autoIncrement: true,
},
user_id: {
type: Sequelize.BIGINT.UNSIGNED,
allowNull: false,
},
plan_id: {
type: Sequelize.BIGINT.UNSIGNED,
allowNull: false,
},
status: {
type: Sequelize.ENUM("pending", "active", "expired", "cancelled"),
allowNull: false,
defaultValue: "pending",
},
start_time: { type: Sequelize.DATE, allowNull: false },
end_time: { type: Sequelize.DATE, allowNull: false },
renew_mode: {
type: Sequelize.ENUM("manual", "auto"),
allowNull: false,
defaultValue: "manual",
},
payment_channel: {
type: Sequelize.ENUM("offline", "pay_link"),
allowNull: true,
},
payment_ref: {
type: Sequelize.STRING(200),
allowNull: true,
},
},
{
tableName: "biz_subscriptions",
timestamps: true,
underscored: true,
comment: "订阅",
}
);
};

View File

@@ -0,0 +1,38 @@
const Sequelize = require("sequelize");
module.exports = (db) => {
return db.define(
"biz_usage_monthly",
{
id: {
type: Sequelize.BIGINT.UNSIGNED,
primaryKey: true,
autoIncrement: true,
},
user_id: {
type: Sequelize.BIGINT.UNSIGNED,
allowNull: false,
},
plan_id: {
type: Sequelize.BIGINT.UNSIGNED,
allowNull: false,
},
stat_month: {
type: Sequelize.STRING(7),
allowNull: false,
comment: "YYYY-MM",
},
msg_count: { type: Sequelize.INTEGER, allowNull: false, defaultValue: 0 },
mass_count: { type: Sequelize.INTEGER, allowNull: false, defaultValue: 0 },
friend_count: { type: Sequelize.INTEGER, allowNull: false, defaultValue: 0 },
sns_count: { type: Sequelize.INTEGER, allowNull: false, defaultValue: 0 },
active_user_count: { type: Sequelize.INTEGER, allowNull: false, defaultValue: 0 },
},
{
tableName: "biz_usage_monthly",
timestamps: true,
underscored: true,
comment: "月用量",
}
);
};

45
api/model/biz_user.js Normal file
View File

@@ -0,0 +1,45 @@
const Sequelize = require("sequelize");
module.exports = (db) => {
return db.define(
"biz_user",
{
id: {
type: Sequelize.BIGINT.UNSIGNED,
primaryKey: true,
autoIncrement: true,
},
name: {
type: Sequelize.STRING(100),
allowNull: false,
defaultValue: "",
comment: "姓名/称呼",
},
mobile: {
type: Sequelize.STRING(20),
allowNull: true,
comment: "手机号",
},
email: {
type: Sequelize.STRING(120),
allowNull: true,
},
company_name: {
type: Sequelize.STRING(200),
allowNull: true,
comment: "公司名",
},
status: {
type: Sequelize.ENUM("active", "disabled"),
allowNull: false,
defaultValue: "active",
},
},
{
tableName: "biz_users",
timestamps: true,
underscored: true,
comment: "业务用户",
}
);
};

View File

@@ -0,0 +1,26 @@
const Sequelize = require("sequelize");
module.exports = (db) => {
return db.define("sys_control_type", {
name: {
type: Sequelize.STRING(100),
allowNull: false,
defaultValue: "",
comment: "控件名称",
},
module_key: {
type: Sequelize.STRING(100),
allowNull: false,
defaultValue: "",
comment: "组件key",
},
data_lenght: {
type: Sequelize.INTEGER(11),
allowNull: false,
defaultValue: "50",
comment: "数据长度",
},
});
};

37
api/model/sys_log.js Normal file
View File

@@ -0,0 +1,37 @@
const Sequelize = require("sequelize");
// db日志管理
module.exports = (db) => {
return db.define("sys_log", {
table_name: {
type: Sequelize.STRING(100),
allowNull: false,
defaultValue: "",
comment: "表名",
},
operate: {
type: Sequelize.STRING(100),
allowNull: false,
defaultValue: "",
comment: "操作",
},
content: {
type: Sequelize.JSON,
allowNull: false,
defaultValue: "",
comment: "内容",
set(value) {
this.setDataValue("content", { value });
},
get() {
let jsonValue = this.getDataValue("content");
if (jsonValue && jsonValue.value !== undefined) {
return jsonValue.value;
} else {
return jsonValue;
}
},
},
});
};

96
api/model/sys_menu.js Normal file
View File

@@ -0,0 +1,96 @@
const Sequelize = require("sequelize");
// 菜单表
module.exports = (db) => {
return db.define("sys_menu", {
// 菜单名称
name: {
type: Sequelize.STRING(100),
allowNull: false,
defaultValue: "",
comment: "菜单名称",
},
// 父id
parent_id: {
type: Sequelize.INTEGER(11).UNSIGNED,
allowNull: true,
defaultValue: 0,
comment: "父id",
},
// 图标
icon: {
type: Sequelize.STRING(100),
allowNull: false,
defaultValue: "",
comment: "图标",
},
path: {
type: Sequelize.STRING(255),
allowNull: false,
defaultValue: "",
comment: "路径",
},
// 菜单类型 "菜单", "页面", "外链", "功能"
type: {
type: Sequelize.STRING(255),
allowNull: false,
defaultValue: "页面",
comment: "菜单类型",
},
//模型id
model_id: {
type: Sequelize.INTEGER(11).UNSIGNED,
allowNull: true,
defaultValue: 0,
comment: "模型id",
},
//表单id
form_id: {
type: Sequelize.INTEGER(11).UNSIGNED,
allowNull: true,
defaultValue: 0,
comment: "表单id",
},
// 组件地址
component: {
type: Sequelize.STRING(100),
allowNull: false,
defaultValue: "",
comment: "组件地址",
},
// api地址
api_path: {
type: Sequelize.STRING(100),
allowNull: false,
defaultValue: "",
comment: "api地址",
},
// 是否显示在菜单中
is_show_menu: {
type: Sequelize.INTEGER(1),
allowNull: false,
defaultValue: true,
comment: "是否显示在菜单中",
},
is_show: {
type: Sequelize.INTEGER(1),
allowNull: false,
defaultValue: true,
comment: "是否展示",
},
// 菜单类型
sort: {
type: Sequelize.INTEGER(11),
allowNull: false,
defaultValue: "0",
comment: "菜单类型",
},
});
};

View File

@@ -0,0 +1,37 @@
const Sequelize = require("sequelize");
// 字典表
module.exports = (db) => {
return db.define("sys_parameter", {
key: {
type: Sequelize.STRING(100),
allowNull: false,
defaultValue: "",
comment: "字典key",
},
value: {
type: Sequelize.STRING(100),
allowNull: false,
defaultValue: "",
comment: "值",
},
remark: {
type: Sequelize.STRING(500),
allowNull: false,
defaultValue: "",
comment: "备注",
},
// 是否允许修改 0 允许1 不允许
is_modified: {
type: Sequelize.INTEGER(2),
allowNull: false,
defaultValue: 0,
comment: "是否允许修改",
},
});
};

41
api/model/sys_role.js Normal file
View File

@@ -0,0 +1,41 @@
const Sequelize = require("sequelize");
//角色表
module.exports = (db) => {
return db.define("sys_role", {
name: {
type: Sequelize.STRING(100),
allowNull: false,
defaultValue: "",
comment: "角色名称",
},
// 0 普通角色 1 系统角色
type: {
type: Sequelize.INTEGER(1),
allowNull: false,
defaultValue: "0",
comment: "角色类型",
},
menus: {
type: Sequelize.JSON,
allowNull: false,
defaultValue: "",
comment: "权限菜单",
set(value) {
this.setDataValue('menus', { value });
},
get() {
let jsonValue = this.getDataValue("menus")
if (jsonValue && jsonValue.value !== undefined) {
return jsonValue.value;
}
else {
return jsonValue
}
}
},
});
};

26
api/model/sys_user.js Normal file
View File

@@ -0,0 +1,26 @@
const Sequelize = require("sequelize");
// 系统用户表
module.exports = (db) => {
return db.define("sys_user", {
name: {
type: Sequelize.STRING(100),
allowNull: false,
defaultValue: "",
comment: "名称",
},
password: {
type: Sequelize.STRING(100),
allowNull: false,
defaultValue: "",
comment: "密码",
},
roleId: {
type: Sequelize.INTEGER,
allowNull: false,
comment: "角色id",
},
});
};

View File

@@ -0,0 +1,189 @@
/**
* 订阅模块通用 CRUD与 admin 约定 POST /{model}/page|add|edit|del GET /{model}/detail|all
*/
const baseModel = require("../../middleware/baseModel");
const { op } = baseModel;
function getRequestBody(ctx) {
if (ctx.request && ctx.request.body && Object.keys(ctx.request.body).length > 0) {
return ctx.request.body;
}
if (typeof ctx.getBody === "function") {
return ctx.getBody() || {};
}
return {};
}
function getModel(modelName) {
const m = baseModel[modelName];
if (!m) {
throw new Error(`模型不存在: ${modelName}`);
}
return m;
}
function normalizeForWrite(model, data, { forCreate } = {}) {
const attrs = model.rawAttributes;
const out = {};
for (const k of Object.keys(data || {})) {
if (!attrs[k]) continue;
let v = data[k];
if (v === "") {
if (k === "id" && forCreate) continue;
if (k.endsWith("_id") || k === "id") {
v = null;
} else if (attrs[k].allowNull) {
v = null;
}
}
if (k === "enabled_features" && typeof v === "string" && v.trim() !== "") {
try {
v = JSON.parse(v);
} catch (e) {
/* 保持原字符串,由 Sequelize 或 DB 报错 */
}
}
out[k] = v;
}
if (forCreate && out.id !== undefined && (out.id === "" || out.id === null)) {
delete out.id;
}
return out;
}
function buildSearchWhere(model, seachOption) {
const key = seachOption && seachOption.key;
const raw = seachOption && seachOption.value;
if (!key || raw === undefined || raw === null) return {};
const str = String(raw).trim();
if (str === "") return {};
const attr = model.rawAttributes[key];
if (!attr) {
return { [key]: { [op.like]: `%${str}%` } };
}
const typeKey = attr.type && attr.type.key;
if (typeKey === "BOOLEAN") {
if (str === "true" || str === "1" || str === "是") return { [key]: true };
if (str === "false" || str === "0" || str === "否") return { [key]: false };
return {};
}
if (typeKey === "ENUM") {
return { [key]: str };
}
if (
typeKey === "INTEGER" ||
typeKey === "BIGINT" ||
typeKey === "FLOAT" ||
typeKey === "DOUBLE" ||
typeKey === "DECIMAL"
) {
const n = Number(str);
if (!Number.isNaN(n)) return { [key]: n };
return {};
}
if (typeKey === "DATE" || typeKey === "DATEONLY") {
return { [key]: str };
}
return { [key]: { [op.like]: `%${str}%` } };
}
async function page(modelName, body) {
const model = getModel(modelName);
const param = body.param || body;
const pageOption = param.pageOption || {};
const seachOption = param.seachOption || {};
const pageNum = parseInt(pageOption.page, 10) || 1;
const pageSize = parseInt(pageOption.pageSize, 10) || 20;
const offset = (pageNum - 1) * pageSize;
const where = buildSearchWhere(model, seachOption);
const { count, rows } = await model.findAndCountAll({
where,
offset,
limit: pageSize,
order: [["id", "DESC"]],
});
return { rows, count };
}
async function add(modelName, body) {
const model = getModel(modelName);
const payload = normalizeForWrite(model, body, { forCreate: true });
const row = await model.create(payload);
return row;
}
async function edit(modelName, body) {
const model = getModel(modelName);
const id = body.id;
if (id === undefined || id === null || id === "") {
throw new Error("缺少 id");
}
const payload = normalizeForWrite(model, body, { forCreate: false });
delete payload.id;
await model.update(payload, { where: { id } });
return {};
}
async function del(modelName, body) {
const model = getModel(modelName);
const id = body.id !== undefined ? body.id : body;
if (id === undefined || id === null || id === "") {
throw new Error("缺少 id");
}
await model.destroy({ where: { id } });
return {};
}
async function detail(modelName, query) {
const model = getModel(modelName);
const id = query && (query.id || query.ID);
if (id === undefined || id === null || id === "") {
throw new Error("缺少 id");
}
const row = await model.findByPk(id);
return row;
}
async function all(modelName) {
const model = getModel(modelName);
const rows = await model.findAll({
limit: 2000,
order: [["id", "DESC"]],
});
return rows;
}
async function exportCsv(modelName, body) {
const model = getModel(modelName);
const param = body.param || body;
const where = buildSearchWhere(model, param.seachOption || {});
const rows = await model.findAll({
where,
limit: 10000,
order: [["id", "DESC"]],
});
return { rows };
}
module.exports = {
page,
add,
edit,
del,
detail,
all,
exportCsv,
getRequestBody,
buildSearchWhere,
};

View File

@@ -0,0 +1,37 @@
const baseModel = require("../../middleware/baseModel");
const logs = require("../../tool/logs_proxy");
/**
* 记录审计(失败不影响主流程)
* @param {object} p
* @param {number} [p.admin_user_id]
* @param {number} [p.biz_user_id]
* @param {string} p.action
* @param {string} [p.resource_type]
* @param {number} [p.resource_id]
* @param {object} [p.detail]
*/
async function logAudit(p) {
try {
await baseModel.biz_audit_log.create({
admin_user_id: p.admin_user_id || null,
biz_user_id: p.biz_user_id || null,
action: p.action,
resource_type: p.resource_type || "",
resource_id: p.resource_id != null ? p.resource_id : null,
detail: p.detail || null,
created_at: new Date(),
});
} catch (e) {
logs.error("[biz_audit] 写入失败", e);
}
}
function pickAdminId(ctx) {
if (!ctx) return null;
const u = ctx.user || ctx.state?.user || ctx.session?.user;
if (u && (u.id != null || u.userId != null)) return u.id != null ? u.id : u.userId;
return null;
}
module.exports = { logAudit, pickAdminId };

View File

@@ -0,0 +1,107 @@
const baseModel = require("../../middleware/baseModel");
const tokenLogic = require("./biz_token_logic");
const usageSvc = require("./biz_usage_service");
function featureAllowed(plan, feature) {
if (!feature) return true;
const feats = plan.enabled_features;
if (feats == null) return true;
if (Array.isArray(feats)) return feats.includes(feature);
if (typeof feats === "object") {
return feats[feature] === true || feats[feature] === 1 || feats[feature] === "1";
}
return false;
}
function normalizeUsageDelta(raw) {
if (!raw || typeof raw !== "object") return {};
return {
msg: usageSvc.num(raw.msg ?? raw.msg_count),
mass: usageSvc.num(raw.mass ?? raw.mass_count),
friend: usageSvc.num(raw.friend ?? raw.friend_count),
sns: usageSvc.num(raw.sns ?? raw.sns_count),
active_user: usageSvc.num(raw.active_user ?? raw.active_user_count),
};
}
function hasPositiveDelta(delta) {
return Object.values(delta).some((v) => usageSvc.num(v) > 0);
}
/**
* 对外鉴权Token + 用户 + 有效订阅 + 功能点 + 可选用量上报与额度
* body: { token, feature?, usage_delta?: { msg?, mass?, ... } }
*/
async function verifyRequest(body) {
const { token, feature } = body || {};
if (!token) {
return { ok: false, error_code: "TOKEN_INVALID", message: "缺少 token" };
}
const hash = tokenLogic.hashPlainToken(token);
const row = await baseModel.biz_api_token.findOne({ where: { token_hash: hash } });
if (!row) {
return { ok: false, error_code: "TOKEN_INVALID", message: "Token 不存在" };
}
if (row.status === "revoked") {
return { ok: false, error_code: "TOKEN_REVOKED", message: "Token 已吊销" };
}
const now = new Date();
if (new Date(row.expire_at) < now) {
return { ok: false, error_code: "TOKEN_EXPIRED", message: "Token 已过期" };
}
const user = await baseModel.biz_user.findByPk(row.user_id);
if (!user || user.status !== "active") {
return { ok: false, error_code: "SUBSCRIPTION_INACTIVE", message: "用户不可用" };
}
const sub = await tokenLogic.findActiveSubscriptionForUser(row.user_id);
if (!sub) {
return { ok: false, error_code: "SUBSCRIPTION_INACTIVE", message: "无有效订阅" };
}
const plan = await baseModel.biz_plan.findByPk(sub.plan_id);
if (!plan || plan.status !== "active") {
return { ok: false, error_code: "SUBSCRIPTION_INACTIVE", message: "套餐不可用" };
}
if (feature && !featureAllowed(plan, feature)) {
return { ok: false, error_code: "FEATURE_NOT_ALLOWED", message: "功能未在套餐内" };
}
const statMonth = usageSvc.currentStatMonth();
let usageRow = await usageSvc.getOrCreateUsage(row.user_id, sub.plan_id, statMonth);
const delta = normalizeUsageDelta(body.usage_delta || body.usage_report);
if (hasPositiveDelta(delta)) {
const q = usageSvc.checkQuotaAfterDelta(plan, usageRow, delta);
if (!q.ok) {
return { ok: false, error_code: q.error_code || "QUOTA_EXCEEDED", message: q.message || "额度不足" };
}
usageRow = await usageSvc.applyDelta(row.user_id, sub.plan_id, statMonth, delta);
}
await row.update({ last_used_at: now });
return {
ok: true,
context: {
user_id: row.user_id,
plan_id: sub.plan_id,
subscription_id: sub.id,
token_id: row.id,
stat_month: statMonth,
usage_snapshot: {
msg_count: usageSvc.num(usageRow.msg_count),
mass_count: usageSvc.num(usageRow.mass_count),
friend_count: usageSvc.num(usageRow.friend_count),
sns_count: usageSvc.num(usageRow.sns_count),
active_user_count: usageSvc.num(usageRow.active_user_count),
},
},
};
}
module.exports = { verifyRequest };

View File

@@ -0,0 +1,47 @@
const baseModel = require("../../middleware/baseModel");
const { op } = baseModel;
async function summary() {
const now = new Date();
const in7 = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000);
const [
userTotal,
userActive,
planActive,
subPending,
subActive,
subExpired,
tokenActive,
renewSoon,
] = await Promise.all([
baseModel.biz_user.count(),
baseModel.biz_user.count({ where: { status: "active" } }),
baseModel.biz_plan.count({ where: { status: "active" } }),
baseModel.biz_subscription.count({ where: { status: "pending" } }),
baseModel.biz_subscription.count({ where: { status: "active" } }),
baseModel.biz_subscription.count({ where: { status: "expired" } }),
baseModel.biz_api_token.count({ where: { status: "active" } }),
baseModel.biz_subscription.count({
where: {
status: "active",
end_time: { [op.between]: [now, in7] },
},
}),
]);
return {
users: { total: userTotal, active: userActive },
plans: { active: planActive },
subscriptions: {
pending: subPending,
active: subActive,
expired: subExpired,
renew_within_7d: renewSoon,
},
tokens: { active: tokenActive },
server_time: now.toISOString(),
};
}
module.exports = { summary };

View File

@@ -0,0 +1,130 @@
/**
* 订阅开通 / 升级 / 续费 / 取消 / 到期扫描 / 支付确认
*/
const baseModel = require("../../middleware/baseModel");
const { op } = baseModel;
async function assertUserActive(userId) {
const u = await baseModel.biz_user.findByPk(userId);
if (!u) throw new Error("用户不存在");
if (u.status !== "active") throw new Error("用户已禁用");
return u;
}
async function assertPlanActive(planId) {
const p = await baseModel.biz_plan.findByPk(planId);
if (!p) throw new Error("套餐不存在");
if (p.status !== "active") throw new Error("套餐未上线");
return p;
}
async function openSubscription(body) {
const {
user_id,
plan_id,
start_time,
end_time,
status = "pending",
renew_mode = "manual",
payment_channel,
payment_ref,
} = body;
await assertUserActive(user_id);
await assertPlanActive(plan_id);
const row = await baseModel.biz_subscription.create({
user_id,
plan_id,
status,
start_time,
end_time,
renew_mode,
payment_channel: payment_channel || null,
payment_ref: payment_ref || null,
});
return row;
}
async function upgradeSubscription(body) {
const { subscription_id, new_plan_id, start_time, end_time } = body;
const sub = await baseModel.biz_subscription.findByPk(subscription_id);
if (!sub) throw new Error("订阅不存在");
await assertPlanActive(new_plan_id);
await sub.update({
plan_id: new_plan_id,
start_time: start_time || sub.start_time,
end_time: end_time || sub.end_time,
});
return sub;
}
async function renewSubscription(body) {
const { subscription_id, end_time } = body;
const sub = await baseModel.biz_subscription.findByPk(subscription_id);
if (!sub) throw new Error("订阅不存在");
await sub.update({
end_time,
status: "active",
});
return sub;
}
async function cancelSubscription(body) {
const { subscription_id } = body;
const sub = await baseModel.biz_subscription.findByPk(subscription_id);
if (!sub) throw new Error("订阅不存在");
await sub.update({ status: "cancelled" });
return sub;
}
/** 每天扫描:将已过期且仍为 active 的订阅置为 expired */
async function expireDueSubscriptions() {
const now = new Date();
const [n] = await baseModel.biz_subscription.update(
{ status: "expired" },
{
where: {
status: "active",
end_time: { [op.lt]: now },
},
}
);
return n;
}
/** 线下确认pending -> active写入 payment_ref */
async function confirmOfflinePayment(body) {
const { subscription_id, payment_ref } = body;
if (!subscription_id) throw new Error("缺少 subscription_id");
const sub = await baseModel.biz_subscription.findByPk(subscription_id);
if (!sub) throw new Error("订阅不存在");
await sub.update({
status: "active",
payment_channel: "offline",
payment_ref: payment_ref || sub.payment_ref,
});
return sub;
}
/** 链接支付确认MVP与线下类似仅标记渠道 */
async function confirmLinkPayment(body) {
const { subscription_id, payment_ref } = body;
if (!subscription_id) throw new Error("缺少 subscription_id");
const sub = await baseModel.biz_subscription.findByPk(subscription_id);
if (!sub) throw new Error("订阅不存在");
await sub.update({
status: "active",
payment_channel: "pay_link",
payment_ref: payment_ref || sub.payment_ref,
});
return sub;
}
module.exports = {
openSubscription,
upgradeSubscription,
renewSubscription,
cancelSubscription,
expireDueSubscriptions,
confirmOfflinePayment,
confirmLinkPayment,
};

View File

@@ -0,0 +1,90 @@
const crypto = require("crypto");
const baseModel = require("../../middleware/baseModel");
const { op } = baseModel;
const MAX_TOKENS_PER_USER = 5;
function hashPlainToken(plain) {
return crypto.createHash("sha256").update(plain, "utf8").digest("hex");
}
function generatePlainToken() {
return `waw_${crypto.randomBytes(24).toString("hex")}`;
}
/** 当前时间在 [start,end] 内且 status=active 的订阅 */
async function findActiveSubscriptionForUser(userId) {
const now = new Date();
return baseModel.biz_subscription.findOne({
where: {
user_id: userId,
status: "active",
start_time: { [op.lte]: now },
end_time: { [op.gte]: now },
},
order: [["id", "DESC"]],
});
}
async function createToken(body) {
const { user_id, token_name, expire_at } = body;
if (!user_id || !expire_at) throw new Error("缺少 user_id 或 expire_at");
const u = await baseModel.biz_user.findByPk(user_id);
if (!u) throw new Error("用户不存在");
if (u.status !== "active") throw new Error("用户已禁用");
const activeCount = await baseModel.biz_api_token.count({
where: { user_id, status: "active" },
});
if (activeCount >= MAX_TOKENS_PER_USER) {
throw new Error(`单用户最多 ${MAX_TOKENS_PER_USER} 个有效 Token`);
}
const sub = await findActiveSubscriptionForUser(user_id);
const plan_id = sub ? sub.plan_id : null;
const plain = generatePlainToken();
const token_hash = hashPlainToken(plain);
const row = await baseModel.biz_api_token.create({
user_id,
plan_id,
token_name: token_name || "default",
token_hash,
status: "active",
expire_at,
});
return {
row,
plain_token: plain,
warn: sub ? null : "当前无生效中的订阅,鉴权将失败",
};
}
async function revokeToken(body) {
const id = body.id;
if (id == null) throw new Error("缺少 id");
const row = await baseModel.biz_api_token.findByPk(id);
if (!row) throw new Error("Token 不存在");
await row.update({ status: "revoked" });
return row;
}
async function revokeAllForUser(userId) {
if (userId == null) throw new Error("缺少 user_id");
const [n] = await baseModel.biz_api_token.update(
{ status: "revoked" },
{ where: { user_id: userId, status: "active" } }
);
return n;
}
module.exports = {
hashPlainToken,
createToken,
revokeToken,
revokeAllForUser,
findActiveSubscriptionForUser,
MAX_TOKENS_PER_USER,
};

View File

@@ -0,0 +1,110 @@
const baseModel = require("../../middleware/baseModel");
const { op } = baseModel;
function currentStatMonth(d = new Date()) {
const y = d.getFullYear();
const m = String(d.getMonth() + 1).padStart(2, "0");
return `${y}-${m}`;
}
function num(v) {
if (v === null || v === undefined || v === "") return 0;
const n = Number(v);
return Number.isNaN(n) ? 0 : n;
}
/** 0 表示无限制(不校验) */
function quotaExceeded(used, limit) {
const li = num(limit);
if (li <= 0) return false;
return num(used) >= li;
}
/**
* 取或创建当月用量行
*/
async function getOrCreateUsage(userId, planId, statMonth) {
const [row] = await baseModel.biz_usage_monthly.findOrCreate({
where: { user_id: userId, stat_month: statMonth },
defaults: {
user_id: userId,
plan_id: planId,
stat_month: statMonth,
msg_count: 0,
mass_count: 0,
friend_count: 0,
sns_count: 0,
active_user_count: 0,
},
});
if (num(row.plan_id) !== num(planId)) {
await row.update({ plan_id: planId });
}
return row;
}
async function applyDelta(userId, planId, statMonth, delta) {
const row = await getOrCreateUsage(userId, planId, statMonth);
const next = {
msg_count: num(row.msg_count) + num(delta.msg),
mass_count: num(row.mass_count) + num(delta.mass),
friend_count: num(row.friend_count) + num(delta.friend),
sns_count: num(row.sns_count) + num(delta.sns),
active_user_count: num(row.active_user_count) + num(delta.active_user),
};
await row.update(next);
return row.reload();
}
/**
* 校验「增量」后是否超限(与套餐额度对比)
* feature: msg | mass | friend | sns | active_user
*/
function checkQuotaAfterDelta(plan, usageRow, delta) {
const checks = [
["msg", "msg_count", "msg_quota"],
["mass", "mass_count", "mass_quota"],
["friend", "friend_count", "friend_quota"],
["sns", "sns_count", "sns_quota"],
["active_user", "active_user_count", "active_user_limit"],
];
for (const [key, uCol, pCol] of checks) {
const add = num(delta[key]);
if (add <= 0) continue;
const used = num(usageRow[uCol]) + add;
if (quotaExceeded(used, plan[pCol])) {
return { ok: false, error_code: "QUOTA_EXCEEDED", message: `额度不足: ${key}` };
}
}
return { ok: true };
}
/**
* 为当前月所有有效订阅补用量行(月结/初始化)
*/
async function ensureUsageRowsForCurrentMonth() {
const statMonth = currentStatMonth();
const now = new Date();
const subs = await baseModel.biz_subscription.findAll({
where: {
status: "active",
start_time: { [op.lte]: now },
end_time: { [op.gte]: now },
},
});
let n = 0;
for (const s of subs) {
await getOrCreateUsage(s.user_id, s.plan_id, statMonth);
n += 1;
}
return n;
}
module.exports = {
currentStatMonth,
getOrCreateUsage,
applyDelta,
checkQuotaAfterDelta,
ensureUsageRowsForCurrentMonth,
num,
};

333
api/service/ossTool.js Normal file
View File

@@ -0,0 +1,333 @@
const OSS = require('ali-oss')
const fs = require('fs')
const config = require('../../config/config')['aliyun']
const uuid = require('node-uuid')
const logs = require('../../tool/logs_proxy')
/**
* OSS 文件上传工具类
* 统一管理文件上传、存储路径、文件类型等
*/
class OSSTool {
constructor() {
this.client = new OSS({
region: 'oss-cn-shanghai',
accessKeyId: config.accessKeyId,
accessKeySecret: config.accessKeySecret,
bucket:config.bucket
})
// 基础存储路径前缀
this.basePrefix = 'app/uploads'
// 文件类型映射
this.fileTypeMap = {
// 图片类型
'image/jpeg': 'jpg',
'image/jpg': 'jpg',
'image/png': 'png',
'image/gif': 'gif',
'image/webp': 'webp',
'image/svg+xml': 'svg',
// 视频类型
'video/mp4': 'mp4',
'video/avi': 'avi',
'video/mov': 'mov',
'video/wmv': 'wmv',
'video/flv': 'flv',
'video/webm': 'webm',
'video/mkv': 'mkv',
// 音频类型
'audio/mp3': 'mp3',
'audio/wav': 'wav',
'audio/aac': 'aac',
'audio/ogg': 'ogg',
'audio/flac': 'flac',
// 文档类型
'application/pdf': 'pdf',
'application/msword': 'doc',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document': 'docx',
'application/vnd.ms-excel': 'xls',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': 'xlsx',
'application/vnd.ms-powerpoint': 'ppt',
'application/vnd.openxmlformats-officedocument.presentationml.presentation': 'pptx',
'text/plain': 'txt',
'text/html': 'html',
'text/css': 'css',
'application/javascript': 'js',
'application/json': 'json'
}
}
/**
* 获取文件后缀名
* @param {Object} file - 文件对象(兼容 formidable 格式)
* @returns {string} 文件后缀名
*/
getFileSuffix(file) {
// 优先使用 MIME 类型判断(兼容 type 和 mimetype
const mimeType = file.mimetype || file.type
if (mimeType && this.fileTypeMap[mimeType]) {
return this.fileTypeMap[mimeType]
}
// 备用方案:从文件名获取(兼容 originalFilename 和 name
const fileName = file.originalFilename || file.name
if (fileName) {
const lastIndex = fileName.lastIndexOf('.')
if (lastIndex > -1) {
return fileName.substring(lastIndex + 1).toLowerCase()
}
}
return 'bin'
}
/**
* 获取文件存储路径
* @param {Object} file - 文件对象(兼容 formidable 格式)
* @param {string} category - 存储分类
* @returns {string} 完整的存储路径
*/
getStoragePath(file, category = 'files') {
const suffix = this.getFileSuffix(file)
const uid = uuid.v4()
// 根据文件类型确定子路径(兼容 mimetype 和 type
let subPath = category
const mimeType = file.mimetype || file.type
if (mimeType) {
if (mimeType.startsWith('image/')) {
subPath = 'images'
} else if (mimeType.startsWith('video/')) {
subPath = 'videos'
} else if (mimeType.startsWith('audio/')) {
subPath = 'audios'
} else if (mimeType.startsWith('application/') || mimeType.startsWith('text/')) {
subPath = 'documents'
}
}
// 完整路径front/ball/{subPath}/{uid}.{suffix}
return `${this.basePrefix}/${subPath}/${uid}.${suffix}`
}
/**
* 核心文件上传方法
* @param {Object} file - 文件对象(兼容 formidable 格式)
* @param {string} category - 存储分类
* @returns {Object} 上传结果
*/
async uploadFile(file, category = 'files') {
try {
// 兼容不同的文件对象格式filepath 或 path
const filePath = file.filepath || file.path
// 验证文件
if (!file || !filePath) {
return { success: false, error: '无效的文件对象' }
}
const stream = fs.createReadStream(filePath)
const storagePath = this.getStoragePath(file, category)
const suffix = this.getFileSuffix(file)
// 设置 content-type兼容 mimetype 和 type
const contentType = file.mimetype || file.type || 'application/octet-stream'
// 上传到 OSS
const result = await this.client.put(storagePath, stream, {
headers: {
'content-disposition': 'inline',
"content-type": contentType
}
})
if (result.res.status === 200) {
const ossPath = config.ossUrl + '/' + result.name
// 上传成功后删除临时文件
try {
if (fs.existsSync(filePath)) {
fs.unlinkSync(filePath)
}
} catch (unlinkError) {
logs.error('删除临时文件失败:', unlinkError)
}
// 使用 ossPathhttps作为 path确保返回 https 格式
const path = ossPath
return {
success: true,
name: result.name,
path: path,
ossPath,
fileType: file.mimetype || file.type,
fileSize: file.size,
originalName: file.originalFilename || file.name,
suffix: suffix,
storagePath: storagePath
}
} else {
return { success: false, error: 'OSS 上传失败' }
}
} catch (error) {
logs.error('文件上传错误:', error)
// 上传失败也要清理临时文件
try {
const filePath = file.filepath || file.path
if (filePath && fs.existsSync(filePath)) {
fs.unlinkSync(filePath)
}
} catch (unlinkError) {
logs.error('删除临时文件失败:', unlinkError)
}
return { success: false, error: error.message }
}
}
/**
* 上传流数据
* @param {Stream} stream - 文件流
* @param {string} contentType - 内容类型
* @param {string} suffix - 文件后缀
* @returns {Object} 上传结果
*/
async uploadStream(stream, contentType, suffix) {
try {
const uid = uuid.v4()
const storagePath = `${this.basePrefix}/files/${uid}.${suffix}`
const result = await this.client.put(storagePath, stream, {
headers: {
'content-disposition': 'inline',
"content-type": contentType
}
})
if (result.res.status === 200) {
const ossPath = config.ossUrl + '/' + result.name
// 使用 ossPathhttps作为 path确保返回 https 格式
const path = ossPath
return {
success: true,
name: result.name,
path: path,
ossPath,
storagePath: storagePath
}
} else {
return { success: false, error: 'OSS 上传失败' }
}
} catch (error) {
logs.error('流上传错误:', error)
return { success: false, error: error.message }
}
}
/**
* 删除文件
* @param {string} filePath - 文件路径
* @returns {Object} 删除结果
*/
async deleteFile(filePath) {
try {
if (!filePath) {
return { success: false, error: '文件路径不能为空' }
}
// 从完整 URL 中提取相对路径
const relativePath = filePath.replace(config.ossUrl + '/', '')
const result = await this.client.delete(relativePath)
if (result.res.status === 204) {
return { success: true, message: '文件删除成功' }
} else {
return { success: false, error: '文件删除失败' }
}
} catch (error) {
logs.error('文件删除错误:', error)
return { success: false, error: error.message }
}
}
/**
* 获取文件信息
* @param {string} filePath - 文件路径
* @returns {Object} 文件信息
*/
async getFileInfo(filePath) {
try {
if (!filePath) {
return { success: false, error: '文件路径不能为空' }
}
const relativePath = filePath.replace(config.ossUrl + '/', '')
const result = await this.client.head(relativePath)
return {
success: true,
size: result.res.headers['content-length'],
type: result.res.headers['content-type'],
lastModified: result.res.headers['last-modified'],
etag: result.res.headers['etag']
}
} catch (error) {
logs.error('获取文件信息错误:', error)
return { success: false, error: error.message }
}
}
// ==================== 便捷方法 ====================
/**
* 上传图片文件(保持向后兼容)
* @param {Object} file - 图片文件
* @returns {Object} 上传结果
*/
async putImg(file) {
return await this.uploadFile(file, 'images')
}
/**
* 上传视频文件
* @param {Object} file - 视频文件
* @returns {Object} 上传结果
*/
async uploadVideo(file) {
return await this.uploadFile(file, 'videos')
}
/**
* 上传音频文件
* @param {Object} file - 音频文件
* @returns {Object} 上传结果
*/
async uploadAudio(file) {
return await this.uploadFile(file, 'audios')
}
/**
* 上传文档文件
* @param {Object} file - 文档文件
* @returns {Object} 上传结果
*/
async uploadDocument(file) {
return await this.uploadFile(file, 'documents')
}
}
// 创建单例实例
const ossTool = new OSSTool()
// 导出实例(保持向后兼容)
module.exports = ossTool

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

6
apidoc/assets/bootstrap.min.css vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 566 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
apidoc/assets/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

View File

@@ -0,0 +1,288 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >
<svg xmlns="http://www.w3.org/2000/svg">
<metadata></metadata>
<defs>
<font id="glyphicons_halflingsregular" horiz-adv-x="1200" >
<font-face units-per-em="1200" ascent="960" descent="-240" />
<missing-glyph horiz-adv-x="500" />
<glyph horiz-adv-x="0" />
<glyph horiz-adv-x="400" />
<glyph unicode=" " />
<glyph unicode="*" d="M600 1100q15 0 34 -1.5t30 -3.5l11 -1q10 -2 17.5 -10.5t7.5 -18.5v-224l158 158q7 7 18 8t19 -6l106 -106q7 -8 6 -19t-8 -18l-158 -158h224q10 0 18.5 -7.5t10.5 -17.5q6 -41 6 -75q0 -15 -1.5 -34t-3.5 -30l-1 -11q-2 -10 -10.5 -17.5t-18.5 -7.5h-224l158 -158 q7 -7 8 -18t-6 -19l-106 -106q-8 -7 -19 -6t-18 8l-158 158v-224q0 -10 -7.5 -18.5t-17.5 -10.5q-41 -6 -75 -6q-15 0 -34 1.5t-30 3.5l-11 1q-10 2 -17.5 10.5t-7.5 18.5v224l-158 -158q-7 -7 -18 -8t-19 6l-106 106q-7 8 -6 19t8 18l158 158h-224q-10 0 -18.5 7.5 t-10.5 17.5q-6 41 -6 75q0 15 1.5 34t3.5 30l1 11q2 10 10.5 17.5t18.5 7.5h224l-158 158q-7 7 -8 18t6 19l106 106q8 7 19 6t18 -8l158 -158v224q0 10 7.5 18.5t17.5 10.5q41 6 75 6z" />
<glyph unicode="+" d="M450 1100h200q21 0 35.5 -14.5t14.5 -35.5v-350h350q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-350v-350q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5v350h-350q-21 0 -35.5 14.5t-14.5 35.5v200q0 21 14.5 35.5t35.5 14.5 h350v350q0 21 14.5 35.5t35.5 14.5z" />
<glyph unicode="&#xa0;" />
<glyph unicode="&#xa5;" d="M825 1100h250q10 0 12.5 -5t-5.5 -13l-364 -364q-6 -6 -11 -18h268q10 0 13 -6t-3 -14l-120 -160q-6 -8 -18 -14t-22 -6h-125v-100h275q10 0 13 -6t-3 -14l-120 -160q-6 -8 -18 -14t-22 -6h-125v-174q0 -11 -7.5 -18.5t-18.5 -7.5h-148q-11 0 -18.5 7.5t-7.5 18.5v174 h-275q-10 0 -13 6t3 14l120 160q6 8 18 14t22 6h125v100h-275q-10 0 -13 6t3 14l120 160q6 8 18 14t22 6h118q-5 12 -11 18l-364 364q-8 8 -5.5 13t12.5 5h250q25 0 43 -18l164 -164q8 -8 18 -8t18 8l164 164q18 18 43 18z" />
<glyph unicode="&#x2000;" horiz-adv-x="650" />
<glyph unicode="&#x2001;" horiz-adv-x="1300" />
<glyph unicode="&#x2002;" horiz-adv-x="650" />
<glyph unicode="&#x2003;" horiz-adv-x="1300" />
<glyph unicode="&#x2004;" horiz-adv-x="433" />
<glyph unicode="&#x2005;" horiz-adv-x="325" />
<glyph unicode="&#x2006;" horiz-adv-x="216" />
<glyph unicode="&#x2007;" horiz-adv-x="216" />
<glyph unicode="&#x2008;" horiz-adv-x="162" />
<glyph unicode="&#x2009;" horiz-adv-x="260" />
<glyph unicode="&#x200a;" horiz-adv-x="72" />
<glyph unicode="&#x202f;" horiz-adv-x="260" />
<glyph unicode="&#x205f;" horiz-adv-x="325" />
<glyph unicode="&#x20ac;" d="M744 1198q242 0 354 -189q60 -104 66 -209h-181q0 45 -17.5 82.5t-43.5 61.5t-58 40.5t-60.5 24t-51.5 7.5q-19 0 -40.5 -5.5t-49.5 -20.5t-53 -38t-49 -62.5t-39 -89.5h379l-100 -100h-300q-6 -50 -6 -100h406l-100 -100h-300q9 -74 33 -132t52.5 -91t61.5 -54.5t59 -29 t47 -7.5q22 0 50.5 7.5t60.5 24.5t58 41t43.5 61t17.5 80h174q-30 -171 -128 -278q-107 -117 -274 -117q-206 0 -324 158q-36 48 -69 133t-45 204h-217l100 100h112q1 47 6 100h-218l100 100h134q20 87 51 153.5t62 103.5q117 141 297 141z" />
<glyph unicode="&#x20bd;" d="M428 1200h350q67 0 120 -13t86 -31t57 -49.5t35 -56.5t17 -64.5t6.5 -60.5t0.5 -57v-16.5v-16.5q0 -36 -0.5 -57t-6.5 -61t-17 -65t-35 -57t-57 -50.5t-86 -31.5t-120 -13h-178l-2 -100h288q10 0 13 -6t-3 -14l-120 -160q-6 -8 -18 -14t-22 -6h-138v-175q0 -11 -5.5 -18 t-15.5 -7h-149q-10 0 -17.5 7.5t-7.5 17.5v175h-267q-10 0 -13 6t3 14l120 160q6 8 18 14t22 6h117v100h-267q-10 0 -13 6t3 14l120 160q6 8 18 14t22 6h117v475q0 10 7.5 17.5t17.5 7.5zM600 1000v-300h203q64 0 86.5 33t22.5 119q0 84 -22.5 116t-86.5 32h-203z" />
<glyph unicode="&#x2212;" d="M250 700h800q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-800q-21 0 -35.5 14.5t-14.5 35.5v200q0 21 14.5 35.5t35.5 14.5z" />
<glyph unicode="&#x231b;" d="M1000 1200v-150q0 -21 -14.5 -35.5t-35.5 -14.5h-50v-100q0 -91 -49.5 -165.5t-130.5 -109.5q81 -35 130.5 -109.5t49.5 -165.5v-150h50q21 0 35.5 -14.5t14.5 -35.5v-150h-800v150q0 21 14.5 35.5t35.5 14.5h50v150q0 91 49.5 165.5t130.5 109.5q-81 35 -130.5 109.5 t-49.5 165.5v100h-50q-21 0 -35.5 14.5t-14.5 35.5v150h800zM400 1000v-100q0 -60 32.5 -109.5t87.5 -73.5q28 -12 44 -37t16 -55t-16 -55t-44 -37q-55 -24 -87.5 -73.5t-32.5 -109.5v-150h400v150q0 60 -32.5 109.5t-87.5 73.5q-28 12 -44 37t-16 55t16 55t44 37 q55 24 87.5 73.5t32.5 109.5v100h-400z" />
<glyph unicode="&#x25fc;" horiz-adv-x="500" d="M0 0z" />
<glyph unicode="&#x2601;" d="M503 1089q110 0 200.5 -59.5t134.5 -156.5q44 14 90 14q120 0 205 -86.5t85 -206.5q0 -121 -85 -207.5t-205 -86.5h-750q-79 0 -135.5 57t-56.5 137q0 69 42.5 122.5t108.5 67.5q-2 12 -2 37q0 153 108 260.5t260 107.5z" />
<glyph unicode="&#x26fa;" d="M774 1193.5q16 -9.5 20.5 -27t-5.5 -33.5l-136 -187l467 -746h30q20 0 35 -18.5t15 -39.5v-42h-1200v42q0 21 15 39.5t35 18.5h30l468 746l-135 183q-10 16 -5.5 34t20.5 28t34 5.5t28 -20.5l111 -148l112 150q9 16 27 20.5t34 -5zM600 200h377l-182 112l-195 534v-646z " />
<glyph unicode="&#x2709;" d="M25 1100h1150q10 0 12.5 -5t-5.5 -13l-564 -567q-8 -8 -18 -8t-18 8l-564 567q-8 8 -5.5 13t12.5 5zM18 882l264 -264q8 -8 8 -18t-8 -18l-264 -264q-8 -8 -13 -5.5t-5 12.5v550q0 10 5 12.5t13 -5.5zM918 618l264 264q8 8 13 5.5t5 -12.5v-550q0 -10 -5 -12.5t-13 5.5 l-264 264q-8 8 -8 18t8 18zM818 482l364 -364q8 -8 5.5 -13t-12.5 -5h-1150q-10 0 -12.5 5t5.5 13l364 364q8 8 18 8t18 -8l164 -164q8 -8 18 -8t18 8l164 164q8 8 18 8t18 -8z" />
<glyph unicode="&#x270f;" d="M1011 1210q19 0 33 -13l153 -153q13 -14 13 -33t-13 -33l-99 -92l-214 214l95 96q13 14 32 14zM1013 800l-615 -614l-214 214l614 614zM317 96l-333 -112l110 335z" />
<glyph unicode="&#xe001;" d="M700 650v-550h250q21 0 35.5 -14.5t14.5 -35.5v-50h-800v50q0 21 14.5 35.5t35.5 14.5h250v550l-500 550h1200z" />
<glyph unicode="&#xe002;" d="M368 1017l645 163q39 15 63 0t24 -49v-831q0 -55 -41.5 -95.5t-111.5 -63.5q-79 -25 -147 -4.5t-86 75t25.5 111.5t122.5 82q72 24 138 8v521l-600 -155v-606q0 -42 -44 -90t-109 -69q-79 -26 -147 -5.5t-86 75.5t25.5 111.5t122.5 82.5q72 24 138 7v639q0 38 14.5 59 t53.5 34z" />
<glyph unicode="&#xe003;" d="M500 1191q100 0 191 -39t156.5 -104.5t104.5 -156.5t39 -191l-1 -2l1 -5q0 -141 -78 -262l275 -274q23 -26 22.5 -44.5t-22.5 -42.5l-59 -58q-26 -20 -46.5 -20t-39.5 20l-275 274q-119 -77 -261 -77l-5 1l-2 -1q-100 0 -191 39t-156.5 104.5t-104.5 156.5t-39 191 t39 191t104.5 156.5t156.5 104.5t191 39zM500 1022q-88 0 -162 -43t-117 -117t-43 -162t43 -162t117 -117t162 -43t162 43t117 117t43 162t-43 162t-117 117t-162 43z" />
<glyph unicode="&#xe005;" d="M649 949q48 68 109.5 104t121.5 38.5t118.5 -20t102.5 -64t71 -100.5t27 -123q0 -57 -33.5 -117.5t-94 -124.5t-126.5 -127.5t-150 -152.5t-146 -174q-62 85 -145.5 174t-150 152.5t-126.5 127.5t-93.5 124.5t-33.5 117.5q0 64 28 123t73 100.5t104 64t119 20 t120.5 -38.5t104.5 -104z" />
<glyph unicode="&#xe006;" d="M407 800l131 353q7 19 17.5 19t17.5 -19l129 -353h421q21 0 24 -8.5t-14 -20.5l-342 -249l130 -401q7 -20 -0.5 -25.5t-24.5 6.5l-343 246l-342 -247q-17 -12 -24.5 -6.5t-0.5 25.5l130 400l-347 251q-17 12 -14 20.5t23 8.5h429z" />
<glyph unicode="&#xe007;" d="M407 800l131 353q7 19 17.5 19t17.5 -19l129 -353h421q21 0 24 -8.5t-14 -20.5l-342 -249l130 -401q7 -20 -0.5 -25.5t-24.5 6.5l-343 246l-342 -247q-17 -12 -24.5 -6.5t-0.5 25.5l130 400l-347 251q-17 12 -14 20.5t23 8.5h429zM477 700h-240l197 -142l-74 -226 l193 139l195 -140l-74 229l192 140h-234l-78 211z" />
<glyph unicode="&#xe008;" d="M600 1200q124 0 212 -88t88 -212v-250q0 -46 -31 -98t-69 -52v-75q0 -10 6 -21.5t15 -17.5l358 -230q9 -5 15 -16.5t6 -21.5v-93q0 -10 -7.5 -17.5t-17.5 -7.5h-1150q-10 0 -17.5 7.5t-7.5 17.5v93q0 10 6 21.5t15 16.5l358 230q9 6 15 17.5t6 21.5v75q-38 0 -69 52 t-31 98v250q0 124 88 212t212 88z" />
<glyph unicode="&#xe009;" d="M25 1100h1150q10 0 17.5 -7.5t7.5 -17.5v-1050q0 -10 -7.5 -17.5t-17.5 -7.5h-1150q-10 0 -17.5 7.5t-7.5 17.5v1050q0 10 7.5 17.5t17.5 7.5zM100 1000v-100h100v100h-100zM875 1000h-550q-10 0 -17.5 -7.5t-7.5 -17.5v-350q0 -10 7.5 -17.5t17.5 -7.5h550 q10 0 17.5 7.5t7.5 17.5v350q0 10 -7.5 17.5t-17.5 7.5zM1000 1000v-100h100v100h-100zM100 800v-100h100v100h-100zM1000 800v-100h100v100h-100zM100 600v-100h100v100h-100zM1000 600v-100h100v100h-100zM875 500h-550q-10 0 -17.5 -7.5t-7.5 -17.5v-350q0 -10 7.5 -17.5 t17.5 -7.5h550q10 0 17.5 7.5t7.5 17.5v350q0 10 -7.5 17.5t-17.5 7.5zM100 400v-100h100v100h-100zM1000 400v-100h100v100h-100zM100 200v-100h100v100h-100zM1000 200v-100h100v100h-100z" />
<glyph unicode="&#xe010;" d="M50 1100h400q21 0 35.5 -14.5t14.5 -35.5v-400q0 -21 -14.5 -35.5t-35.5 -14.5h-400q-21 0 -35.5 14.5t-14.5 35.5v400q0 21 14.5 35.5t35.5 14.5zM650 1100h400q21 0 35.5 -14.5t14.5 -35.5v-400q0 -21 -14.5 -35.5t-35.5 -14.5h-400q-21 0 -35.5 14.5t-14.5 35.5v400 q0 21 14.5 35.5t35.5 14.5zM50 500h400q21 0 35.5 -14.5t14.5 -35.5v-400q0 -21 -14.5 -35.5t-35.5 -14.5h-400q-21 0 -35.5 14.5t-14.5 35.5v400q0 21 14.5 35.5t35.5 14.5zM650 500h400q21 0 35.5 -14.5t14.5 -35.5v-400q0 -21 -14.5 -35.5t-35.5 -14.5h-400 q-21 0 -35.5 14.5t-14.5 35.5v400q0 21 14.5 35.5t35.5 14.5z" />
<glyph unicode="&#xe011;" d="M50 1100h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5v200q0 21 14.5 35.5t35.5 14.5zM450 1100h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5v200 q0 21 14.5 35.5t35.5 14.5zM850 1100h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5v200q0 21 14.5 35.5t35.5 14.5zM50 700h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200 q-21 0 -35.5 14.5t-14.5 35.5v200q0 21 14.5 35.5t35.5 14.5zM450 700h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5v200q0 21 14.5 35.5t35.5 14.5zM850 700h200q21 0 35.5 -14.5t14.5 -35.5v-200 q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5v200q0 21 14.5 35.5t35.5 14.5zM50 300h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5v200q0 21 14.5 35.5t35.5 14.5zM450 300h200 q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5v200q0 21 14.5 35.5t35.5 14.5zM850 300h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5v200q0 21 14.5 35.5 t35.5 14.5z" />
<glyph unicode="&#xe012;" d="M50 1100h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5v200q0 21 14.5 35.5t35.5 14.5zM450 1100h700q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-700q-21 0 -35.5 14.5t-14.5 35.5v200 q0 21 14.5 35.5t35.5 14.5zM50 700h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5v200q0 21 14.5 35.5t35.5 14.5zM450 700h700q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-700 q-21 0 -35.5 14.5t-14.5 35.5v200q0 21 14.5 35.5t35.5 14.5zM50 300h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5v200q0 21 14.5 35.5t35.5 14.5zM450 300h700q21 0 35.5 -14.5t14.5 -35.5v-200 q0 -21 -14.5 -35.5t-35.5 -14.5h-700q-21 0 -35.5 14.5t-14.5 35.5v200q0 21 14.5 35.5t35.5 14.5z" />
<glyph unicode="&#xe013;" d="M465 477l571 571q8 8 18 8t17 -8l177 -177q8 -7 8 -17t-8 -18l-783 -784q-7 -8 -17.5 -8t-17.5 8l-384 384q-8 8 -8 18t8 17l177 177q7 8 17 8t18 -8l171 -171q7 -7 18 -7t18 7z" />
<glyph unicode="&#xe014;" d="M904 1083l178 -179q8 -8 8 -18.5t-8 -17.5l-267 -268l267 -268q8 -7 8 -17.5t-8 -18.5l-178 -178q-8 -8 -18.5 -8t-17.5 8l-268 267l-268 -267q-7 -8 -17.5 -8t-18.5 8l-178 178q-8 8 -8 18.5t8 17.5l267 268l-267 268q-8 7 -8 17.5t8 18.5l178 178q8 8 18.5 8t17.5 -8 l268 -267l268 268q7 7 17.5 7t18.5 -7z" />
<glyph unicode="&#xe015;" d="M507 1177q98 0 187.5 -38.5t154.5 -103.5t103.5 -154.5t38.5 -187.5q0 -141 -78 -262l300 -299q8 -8 8 -18.5t-8 -18.5l-109 -108q-7 -8 -17.5 -8t-18.5 8l-300 299q-119 -77 -261 -77q-98 0 -188 38.5t-154.5 103t-103 154.5t-38.5 188t38.5 187.5t103 154.5 t154.5 103.5t188 38.5zM506.5 1023q-89.5 0 -165.5 -44t-120 -120.5t-44 -166t44 -165.5t120 -120t165.5 -44t166 44t120.5 120t44 165.5t-44 166t-120.5 120.5t-166 44zM425 900h150q10 0 17.5 -7.5t7.5 -17.5v-75h75q10 0 17.5 -7.5t7.5 -17.5v-150q0 -10 -7.5 -17.5 t-17.5 -7.5h-75v-75q0 -10 -7.5 -17.5t-17.5 -7.5h-150q-10 0 -17.5 7.5t-7.5 17.5v75h-75q-10 0 -17.5 7.5t-7.5 17.5v150q0 10 7.5 17.5t17.5 7.5h75v75q0 10 7.5 17.5t17.5 7.5z" />
<glyph unicode="&#xe016;" d="M507 1177q98 0 187.5 -38.5t154.5 -103.5t103.5 -154.5t38.5 -187.5q0 -141 -78 -262l300 -299q8 -8 8 -18.5t-8 -18.5l-109 -108q-7 -8 -17.5 -8t-18.5 8l-300 299q-119 -77 -261 -77q-98 0 -188 38.5t-154.5 103t-103 154.5t-38.5 188t38.5 187.5t103 154.5 t154.5 103.5t188 38.5zM506.5 1023q-89.5 0 -165.5 -44t-120 -120.5t-44 -166t44 -165.5t120 -120t165.5 -44t166 44t120.5 120t44 165.5t-44 166t-120.5 120.5t-166 44zM325 800h350q10 0 17.5 -7.5t7.5 -17.5v-150q0 -10 -7.5 -17.5t-17.5 -7.5h-350q-10 0 -17.5 7.5 t-7.5 17.5v150q0 10 7.5 17.5t17.5 7.5z" />
<glyph unicode="&#xe017;" d="M550 1200h100q21 0 35.5 -14.5t14.5 -35.5v-400q0 -21 -14.5 -35.5t-35.5 -14.5h-100q-21 0 -35.5 14.5t-14.5 35.5v400q0 21 14.5 35.5t35.5 14.5zM800 975v166q167 -62 272 -209.5t105 -331.5q0 -117 -45.5 -224t-123 -184.5t-184.5 -123t-224 -45.5t-224 45.5 t-184.5 123t-123 184.5t-45.5 224q0 184 105 331.5t272 209.5v-166q-103 -55 -165 -155t-62 -220q0 -116 57 -214.5t155.5 -155.5t214.5 -57t214.5 57t155.5 155.5t57 214.5q0 120 -62 220t-165 155z" />
<glyph unicode="&#xe018;" d="M1025 1200h150q10 0 17.5 -7.5t7.5 -17.5v-1150q0 -10 -7.5 -17.5t-17.5 -7.5h-150q-10 0 -17.5 7.5t-7.5 17.5v1150q0 10 7.5 17.5t17.5 7.5zM725 800h150q10 0 17.5 -7.5t7.5 -17.5v-750q0 -10 -7.5 -17.5t-17.5 -7.5h-150q-10 0 -17.5 7.5t-7.5 17.5v750 q0 10 7.5 17.5t17.5 7.5zM425 500h150q10 0 17.5 -7.5t7.5 -17.5v-450q0 -10 -7.5 -17.5t-17.5 -7.5h-150q-10 0 -17.5 7.5t-7.5 17.5v450q0 10 7.5 17.5t17.5 7.5zM125 300h150q10 0 17.5 -7.5t7.5 -17.5v-250q0 -10 -7.5 -17.5t-17.5 -7.5h-150q-10 0 -17.5 7.5t-7.5 17.5 v250q0 10 7.5 17.5t17.5 7.5z" />
<glyph unicode="&#xe019;" d="M600 1174q33 0 74 -5l38 -152l5 -1q49 -14 94 -39l5 -2l134 80q61 -48 104 -105l-80 -134l3 -5q25 -44 39 -93l1 -6l152 -38q5 -43 5 -73q0 -34 -5 -74l-152 -38l-1 -6q-15 -49 -39 -93l-3 -5l80 -134q-48 -61 -104 -105l-134 81l-5 -3q-44 -25 -94 -39l-5 -2l-38 -151 q-43 -5 -74 -5q-33 0 -74 5l-38 151l-5 2q-49 14 -94 39l-5 3l-134 -81q-60 48 -104 105l80 134l-3 5q-25 45 -38 93l-2 6l-151 38q-6 42 -6 74q0 33 6 73l151 38l2 6q13 48 38 93l3 5l-80 134q47 61 105 105l133 -80l5 2q45 25 94 39l5 1l38 152q43 5 74 5zM600 815 q-89 0 -152 -63t-63 -151.5t63 -151.5t152 -63t152 63t63 151.5t-63 151.5t-152 63z" />
<glyph unicode="&#xe020;" d="M500 1300h300q41 0 70.5 -29.5t29.5 -70.5v-100h275q10 0 17.5 -7.5t7.5 -17.5v-75h-1100v75q0 10 7.5 17.5t17.5 7.5h275v100q0 41 29.5 70.5t70.5 29.5zM500 1200v-100h300v100h-300zM1100 900v-800q0 -41 -29.5 -70.5t-70.5 -29.5h-700q-41 0 -70.5 29.5t-29.5 70.5 v800h900zM300 800v-700h100v700h-100zM500 800v-700h100v700h-100zM700 800v-700h100v700h-100zM900 800v-700h100v700h-100z" />
<glyph unicode="&#xe021;" d="M18 618l620 608q8 7 18.5 7t17.5 -7l608 -608q8 -8 5.5 -13t-12.5 -5h-175v-575q0 -10 -7.5 -17.5t-17.5 -7.5h-250q-10 0 -17.5 7.5t-7.5 17.5v375h-300v-375q0 -10 -7.5 -17.5t-17.5 -7.5h-250q-10 0 -17.5 7.5t-7.5 17.5v575h-175q-10 0 -12.5 5t5.5 13z" />
<glyph unicode="&#xe022;" d="M600 1200v-400q0 -41 29.5 -70.5t70.5 -29.5h300v-650q0 -21 -14.5 -35.5t-35.5 -14.5h-800q-21 0 -35.5 14.5t-14.5 35.5v1100q0 21 14.5 35.5t35.5 14.5h450zM1000 800h-250q-21 0 -35.5 14.5t-14.5 35.5v250z" />
<glyph unicode="&#xe023;" d="M600 1177q117 0 224 -45.5t184.5 -123t123 -184.5t45.5 -224t-45.5 -224t-123 -184.5t-184.5 -123t-224 -45.5t-224 45.5t-184.5 123t-123 184.5t-45.5 224t45.5 224t123 184.5t184.5 123t224 45.5zM600 1027q-116 0 -214.5 -57t-155.5 -155.5t-57 -214.5t57 -214.5 t155.5 -155.5t214.5 -57t214.5 57t155.5 155.5t57 214.5t-57 214.5t-155.5 155.5t-214.5 57zM525 900h50q10 0 17.5 -7.5t7.5 -17.5v-275h175q10 0 17.5 -7.5t7.5 -17.5v-50q0 -10 -7.5 -17.5t-17.5 -7.5h-250q-10 0 -17.5 7.5t-7.5 17.5v350q0 10 7.5 17.5t17.5 7.5z" />
<glyph unicode="&#xe024;" d="M1300 0h-538l-41 400h-242l-41 -400h-538l431 1200h209l-21 -300h162l-20 300h208zM515 800l-27 -300h224l-27 300h-170z" />
<glyph unicode="&#xe025;" d="M550 1200h200q21 0 35.5 -14.5t14.5 -35.5v-450h191q20 0 25.5 -11.5t-7.5 -27.5l-327 -400q-13 -16 -32 -16t-32 16l-327 400q-13 16 -7.5 27.5t25.5 11.5h191v450q0 21 14.5 35.5t35.5 14.5zM1125 400h50q10 0 17.5 -7.5t7.5 -17.5v-350q0 -10 -7.5 -17.5t-17.5 -7.5 h-1050q-10 0 -17.5 7.5t-7.5 17.5v350q0 10 7.5 17.5t17.5 7.5h50q10 0 17.5 -7.5t7.5 -17.5v-175h900v175q0 10 7.5 17.5t17.5 7.5z" />
<glyph unicode="&#xe026;" d="M600 1177q117 0 224 -45.5t184.5 -123t123 -184.5t45.5 -224t-45.5 -224t-123 -184.5t-184.5 -123t-224 -45.5t-224 45.5t-184.5 123t-123 184.5t-45.5 224t45.5 224t123 184.5t184.5 123t224 45.5zM600 1027q-116 0 -214.5 -57t-155.5 -155.5t-57 -214.5t57 -214.5 t155.5 -155.5t214.5 -57t214.5 57t155.5 155.5t57 214.5t-57 214.5t-155.5 155.5t-214.5 57zM525 900h150q10 0 17.5 -7.5t7.5 -17.5v-275h137q21 0 26 -11.5t-8 -27.5l-223 -275q-13 -16 -32 -16t-32 16l-223 275q-13 16 -8 27.5t26 11.5h137v275q0 10 7.5 17.5t17.5 7.5z " />
<glyph unicode="&#xe027;" d="M600 1177q117 0 224 -45.5t184.5 -123t123 -184.5t45.5 -224t-45.5 -224t-123 -184.5t-184.5 -123t-224 -45.5t-224 45.5t-184.5 123t-123 184.5t-45.5 224t45.5 224t123 184.5t184.5 123t224 45.5zM600 1027q-116 0 -214.5 -57t-155.5 -155.5t-57 -214.5t57 -214.5 t155.5 -155.5t214.5 -57t214.5 57t155.5 155.5t57 214.5t-57 214.5t-155.5 155.5t-214.5 57zM632 914l223 -275q13 -16 8 -27.5t-26 -11.5h-137v-275q0 -10 -7.5 -17.5t-17.5 -7.5h-150q-10 0 -17.5 7.5t-7.5 17.5v275h-137q-21 0 -26 11.5t8 27.5l223 275q13 16 32 16 t32 -16z" />
<glyph unicode="&#xe028;" d="M225 1200h750q10 0 19.5 -7t12.5 -17l186 -652q7 -24 7 -49v-425q0 -12 -4 -27t-9 -17q-12 -6 -37 -6h-1100q-12 0 -27 4t-17 8q-6 13 -6 38l1 425q0 25 7 49l185 652q3 10 12.5 17t19.5 7zM878 1000h-556q-10 0 -19 -7t-11 -18l-87 -450q-2 -11 4 -18t16 -7h150 q10 0 19.5 -7t11.5 -17l38 -152q2 -10 11.5 -17t19.5 -7h250q10 0 19.5 7t11.5 17l38 152q2 10 11.5 17t19.5 7h150q10 0 16 7t4 18l-87 450q-2 11 -11 18t-19 7z" />
<glyph unicode="&#xe029;" d="M600 1177q117 0 224 -45.5t184.5 -123t123 -184.5t45.5 -224t-45.5 -224t-123 -184.5t-184.5 -123t-224 -45.5t-224 45.5t-184.5 123t-123 184.5t-45.5 224t45.5 224t123 184.5t184.5 123t224 45.5zM600 1027q-116 0 -214.5 -57t-155.5 -155.5t-57 -214.5t57 -214.5 t155.5 -155.5t214.5 -57t214.5 57t155.5 155.5t57 214.5t-57 214.5t-155.5 155.5t-214.5 57zM540 820l253 -190q17 -12 17 -30t-17 -30l-253 -190q-16 -12 -28 -6.5t-12 26.5v400q0 21 12 26.5t28 -6.5z" />
<glyph unicode="&#xe030;" d="M947 1060l135 135q7 7 12.5 5t5.5 -13v-362q0 -10 -7.5 -17.5t-17.5 -7.5h-362q-11 0 -13 5.5t5 12.5l133 133q-109 76 -238 76q-116 0 -214.5 -57t-155.5 -155.5t-57 -214.5t57 -214.5t155.5 -155.5t214.5 -57t214.5 57t155.5 155.5t57 214.5h150q0 -117 -45.5 -224 t-123 -184.5t-184.5 -123t-224 -45.5t-224 45.5t-184.5 123t-123 184.5t-45.5 224t45.5 224t123 184.5t184.5 123t224 45.5q192 0 347 -117z" />
<glyph unicode="&#xe031;" d="M947 1060l135 135q7 7 12.5 5t5.5 -13v-361q0 -11 -7.5 -18.5t-18.5 -7.5h-361q-11 0 -13 5.5t5 12.5l134 134q-110 75 -239 75q-116 0 -214.5 -57t-155.5 -155.5t-57 -214.5h-150q0 117 45.5 224t123 184.5t184.5 123t224 45.5q192 0 347 -117zM1027 600h150 q0 -117 -45.5 -224t-123 -184.5t-184.5 -123t-224 -45.5q-192 0 -348 118l-134 -134q-7 -8 -12.5 -5.5t-5.5 12.5v360q0 11 7.5 18.5t18.5 7.5h360q10 0 12.5 -5.5t-5.5 -12.5l-133 -133q110 -76 240 -76q116 0 214.5 57t155.5 155.5t57 214.5z" />
<glyph unicode="&#xe032;" d="M125 1200h1050q10 0 17.5 -7.5t7.5 -17.5v-1150q0 -10 -7.5 -17.5t-17.5 -7.5h-1050q-10 0 -17.5 7.5t-7.5 17.5v1150q0 10 7.5 17.5t17.5 7.5zM1075 1000h-850q-10 0 -17.5 -7.5t-7.5 -17.5v-850q0 -10 7.5 -17.5t17.5 -7.5h850q10 0 17.5 7.5t7.5 17.5v850 q0 10 -7.5 17.5t-17.5 7.5zM325 900h50q10 0 17.5 -7.5t7.5 -17.5v-50q0 -10 -7.5 -17.5t-17.5 -7.5h-50q-10 0 -17.5 7.5t-7.5 17.5v50q0 10 7.5 17.5t17.5 7.5zM525 900h450q10 0 17.5 -7.5t7.5 -17.5v-50q0 -10 -7.5 -17.5t-17.5 -7.5h-450q-10 0 -17.5 7.5t-7.5 17.5v50 q0 10 7.5 17.5t17.5 7.5zM325 700h50q10 0 17.5 -7.5t7.5 -17.5v-50q0 -10 -7.5 -17.5t-17.5 -7.5h-50q-10 0 -17.5 7.5t-7.5 17.5v50q0 10 7.5 17.5t17.5 7.5zM525 700h450q10 0 17.5 -7.5t7.5 -17.5v-50q0 -10 -7.5 -17.5t-17.5 -7.5h-450q-10 0 -17.5 7.5t-7.5 17.5v50 q0 10 7.5 17.5t17.5 7.5zM325 500h50q10 0 17.5 -7.5t7.5 -17.5v-50q0 -10 -7.5 -17.5t-17.5 -7.5h-50q-10 0 -17.5 7.5t-7.5 17.5v50q0 10 7.5 17.5t17.5 7.5zM525 500h450q10 0 17.5 -7.5t7.5 -17.5v-50q0 -10 -7.5 -17.5t-17.5 -7.5h-450q-10 0 -17.5 7.5t-7.5 17.5v50 q0 10 7.5 17.5t17.5 7.5zM325 300h50q10 0 17.5 -7.5t7.5 -17.5v-50q0 -10 -7.5 -17.5t-17.5 -7.5h-50q-10 0 -17.5 7.5t-7.5 17.5v50q0 10 7.5 17.5t17.5 7.5zM525 300h450q10 0 17.5 -7.5t7.5 -17.5v-50q0 -10 -7.5 -17.5t-17.5 -7.5h-450q-10 0 -17.5 7.5t-7.5 17.5v50 q0 10 7.5 17.5t17.5 7.5z" />
<glyph unicode="&#xe033;" d="M900 800v200q0 83 -58.5 141.5t-141.5 58.5h-300q-82 0 -141 -59t-59 -141v-200h-100q-41 0 -70.5 -29.5t-29.5 -70.5v-600q0 -41 29.5 -70.5t70.5 -29.5h900q41 0 70.5 29.5t29.5 70.5v600q0 41 -29.5 70.5t-70.5 29.5h-100zM400 800v150q0 21 15 35.5t35 14.5h200 q20 0 35 -14.5t15 -35.5v-150h-300z" />
<glyph unicode="&#xe034;" d="M125 1100h50q10 0 17.5 -7.5t7.5 -17.5v-1075h-100v1075q0 10 7.5 17.5t17.5 7.5zM1075 1052q4 0 9 -2q16 -6 16 -23v-421q0 -6 -3 -12q-33 -59 -66.5 -99t-65.5 -58t-56.5 -24.5t-52.5 -6.5q-26 0 -57.5 6.5t-52.5 13.5t-60 21q-41 15 -63 22.5t-57.5 15t-65.5 7.5 q-85 0 -160 -57q-7 -5 -15 -5q-6 0 -11 3q-14 7 -14 22v438q22 55 82 98.5t119 46.5q23 2 43 0.5t43 -7t32.5 -8.5t38 -13t32.5 -11q41 -14 63.5 -21t57 -14t63.5 -7q103 0 183 87q7 8 18 8z" />
<glyph unicode="&#xe035;" d="M600 1175q116 0 227 -49.5t192.5 -131t131 -192.5t49.5 -227v-300q0 -10 -7.5 -17.5t-17.5 -7.5h-50q-10 0 -17.5 7.5t-7.5 17.5v300q0 127 -70.5 231.5t-184.5 161.5t-245 57t-245 -57t-184.5 -161.5t-70.5 -231.5v-300q0 -10 -7.5 -17.5t-17.5 -7.5h-50 q-10 0 -17.5 7.5t-7.5 17.5v300q0 116 49.5 227t131 192.5t192.5 131t227 49.5zM220 500h160q8 0 14 -6t6 -14v-460q0 -8 -6 -14t-14 -6h-160q-8 0 -14 6t-6 14v460q0 8 6 14t14 6zM820 500h160q8 0 14 -6t6 -14v-460q0 -8 -6 -14t-14 -6h-160q-8 0 -14 6t-6 14v460 q0 8 6 14t14 6z" />
<glyph unicode="&#xe036;" d="M321 814l258 172q9 6 15 2.5t6 -13.5v-750q0 -10 -6 -13.5t-15 2.5l-258 172q-21 14 -46 14h-250q-10 0 -17.5 7.5t-7.5 17.5v350q0 10 7.5 17.5t17.5 7.5h250q25 0 46 14zM900 668l120 120q7 7 17 7t17 -7l34 -34q7 -7 7 -17t-7 -17l-120 -120l120 -120q7 -7 7 -17 t-7 -17l-34 -34q-7 -7 -17 -7t-17 7l-120 119l-120 -119q-7 -7 -17 -7t-17 7l-34 34q-7 7 -7 17t7 17l119 120l-119 120q-7 7 -7 17t7 17l34 34q7 8 17 8t17 -8z" />
<glyph unicode="&#xe037;" d="M321 814l258 172q9 6 15 2.5t6 -13.5v-750q0 -10 -6 -13.5t-15 2.5l-258 172q-21 14 -46 14h-250q-10 0 -17.5 7.5t-7.5 17.5v350q0 10 7.5 17.5t17.5 7.5h250q25 0 46 14zM766 900h4q10 -1 16 -10q96 -129 96 -290q0 -154 -90 -281q-6 -9 -17 -10l-3 -1q-9 0 -16 6 l-29 23q-7 7 -8.5 16.5t4.5 17.5q72 103 72 229q0 132 -78 238q-6 8 -4.5 18t9.5 17l29 22q7 5 15 5z" />
<glyph unicode="&#xe038;" d="M967 1004h3q11 -1 17 -10q135 -179 135 -396q0 -105 -34 -206.5t-98 -185.5q-7 -9 -17 -10h-3q-9 0 -16 6l-42 34q-8 6 -9 16t5 18q111 150 111 328q0 90 -29.5 176t-84.5 157q-6 9 -5 19t10 16l42 33q7 5 15 5zM321 814l258 172q9 6 15 2.5t6 -13.5v-750q0 -10 -6 -13.5 t-15 2.5l-258 172q-21 14 -46 14h-250q-10 0 -17.5 7.5t-7.5 17.5v350q0 10 7.5 17.5t17.5 7.5h250q25 0 46 14zM766 900h4q10 -1 16 -10q96 -129 96 -290q0 -154 -90 -281q-6 -9 -17 -10l-3 -1q-9 0 -16 6l-29 23q-7 7 -8.5 16.5t4.5 17.5q72 103 72 229q0 132 -78 238 q-6 8 -4.5 18.5t9.5 16.5l29 22q7 5 15 5z" />
<glyph unicode="&#xe039;" d="M500 900h100v-100h-100v-100h-400v-100h-100v600h500v-300zM1200 700h-200v-100h200v-200h-300v300h-200v300h-100v200h600v-500zM100 1100v-300h300v300h-300zM800 1100v-300h300v300h-300zM300 900h-100v100h100v-100zM1000 900h-100v100h100v-100zM300 500h200v-500 h-500v500h200v100h100v-100zM800 300h200v-100h-100v-100h-200v100h-100v100h100v200h-200v100h300v-300zM100 400v-300h300v300h-300zM300 200h-100v100h100v-100zM1200 200h-100v100h100v-100zM700 0h-100v100h100v-100zM1200 0h-300v100h300v-100z" />
<glyph unicode="&#xe040;" d="M100 200h-100v1000h100v-1000zM300 200h-100v1000h100v-1000zM700 200h-200v1000h200v-1000zM900 200h-100v1000h100v-1000zM1200 200h-200v1000h200v-1000zM400 0h-300v100h300v-100zM600 0h-100v91h100v-91zM800 0h-100v91h100v-91zM1100 0h-200v91h200v-91z" />
<glyph unicode="&#xe041;" d="M500 1200l682 -682q8 -8 8 -18t-8 -18l-464 -464q-8 -8 -18 -8t-18 8l-682 682l1 475q0 10 7.5 17.5t17.5 7.5h474zM319.5 1024.5q-29.5 29.5 -71 29.5t-71 -29.5t-29.5 -71.5t29.5 -71.5t71 -29.5t71 29.5t29.5 71.5t-29.5 71.5z" />
<glyph unicode="&#xe042;" d="M500 1200l682 -682q8 -8 8 -18t-8 -18l-464 -464q-8 -8 -18 -8t-18 8l-682 682l1 475q0 10 7.5 17.5t17.5 7.5h474zM800 1200l682 -682q8 -8 8 -18t-8 -18l-464 -464q-8 -8 -18 -8t-18 8l-56 56l424 426l-700 700h150zM319.5 1024.5q-29.5 29.5 -71 29.5t-71 -29.5 t-29.5 -71.5t29.5 -71.5t71 -29.5t71 29.5t29.5 71.5t-29.5 71.5z" />
<glyph unicode="&#xe043;" d="M300 1200h825q75 0 75 -75v-900q0 -25 -18 -43l-64 -64q-8 -8 -13 -5.5t-5 12.5v950q0 10 -7.5 17.5t-17.5 7.5h-700q-25 0 -43 -18l-64 -64q-8 -8 -5.5 -13t12.5 -5h700q10 0 17.5 -7.5t7.5 -17.5v-950q0 -10 -7.5 -17.5t-17.5 -7.5h-850q-10 0 -17.5 7.5t-7.5 17.5v975 q0 25 18 43l139 139q18 18 43 18z" />
<glyph unicode="&#xe044;" d="M250 1200h800q21 0 35.5 -14.5t14.5 -35.5v-1150l-450 444l-450 -445v1151q0 21 14.5 35.5t35.5 14.5z" />
<glyph unicode="&#xe045;" d="M822 1200h-444q-11 0 -19 -7.5t-9 -17.5l-78 -301q-7 -24 7 -45l57 -108q6 -9 17.5 -15t21.5 -6h450q10 0 21.5 6t17.5 15l62 108q14 21 7 45l-83 301q-1 10 -9 17.5t-19 7.5zM1175 800h-150q-10 0 -21 -6.5t-15 -15.5l-78 -156q-4 -9 -15 -15.5t-21 -6.5h-550 q-10 0 -21 6.5t-15 15.5l-78 156q-4 9 -15 15.5t-21 6.5h-150q-10 0 -17.5 -7.5t-7.5 -17.5v-650q0 -10 7.5 -17.5t17.5 -7.5h150q10 0 17.5 7.5t7.5 17.5v150q0 10 7.5 17.5t17.5 7.5h750q10 0 17.5 -7.5t7.5 -17.5v-150q0 -10 7.5 -17.5t17.5 -7.5h150q10 0 17.5 7.5 t7.5 17.5v650q0 10 -7.5 17.5t-17.5 7.5zM850 200h-500q-10 0 -19.5 -7t-11.5 -17l-38 -152q-2 -10 3.5 -17t15.5 -7h600q10 0 15.5 7t3.5 17l-38 152q-2 10 -11.5 17t-19.5 7z" />
<glyph unicode="&#xe046;" d="M500 1100h200q56 0 102.5 -20.5t72.5 -50t44 -59t25 -50.5l6 -20h150q41 0 70.5 -29.5t29.5 -70.5v-600q0 -41 -29.5 -70.5t-70.5 -29.5h-1000q-41 0 -70.5 29.5t-29.5 70.5v600q0 41 29.5 70.5t70.5 29.5h150q2 8 6.5 21.5t24 48t45 61t72 48t102.5 21.5zM900 800v-100 h100v100h-100zM600 730q-95 0 -162.5 -67.5t-67.5 -162.5t67.5 -162.5t162.5 -67.5t162.5 67.5t67.5 162.5t-67.5 162.5t-162.5 67.5zM600 603q43 0 73 -30t30 -73t-30 -73t-73 -30t-73 30t-30 73t30 73t73 30z" />
<glyph unicode="&#xe047;" d="M681 1199l385 -998q20 -50 60 -92q18 -19 36.5 -29.5t27.5 -11.5l10 -2v-66h-417v66q53 0 75 43.5t5 88.5l-82 222h-391q-58 -145 -92 -234q-11 -34 -6.5 -57t25.5 -37t46 -20t55 -6v-66h-365v66q56 24 84 52q12 12 25 30.5t20 31.5l7 13l399 1006h93zM416 521h340 l-162 457z" />
<glyph unicode="&#xe048;" d="M753 641q5 -1 14.5 -4.5t36 -15.5t50.5 -26.5t53.5 -40t50.5 -54.5t35.5 -70t14.5 -87q0 -67 -27.5 -125.5t-71.5 -97.5t-98.5 -66.5t-108.5 -40.5t-102 -13h-500v89q41 7 70.5 32.5t29.5 65.5v827q0 24 -0.5 34t-3.5 24t-8.5 19.5t-17 13.5t-28 12.5t-42.5 11.5v71 l471 -1q57 0 115.5 -20.5t108 -57t80.5 -94t31 -124.5q0 -51 -15.5 -96.5t-38 -74.5t-45 -50.5t-38.5 -30.5zM400 700h139q78 0 130.5 48.5t52.5 122.5q0 41 -8.5 70.5t-29.5 55.5t-62.5 39.5t-103.5 13.5h-118v-350zM400 200h216q80 0 121 50.5t41 130.5q0 90 -62.5 154.5 t-156.5 64.5h-159v-400z" />
<glyph unicode="&#xe049;" d="M877 1200l2 -57q-83 -19 -116 -45.5t-40 -66.5l-132 -839q-9 -49 13 -69t96 -26v-97h-500v97q186 16 200 98l173 832q3 17 3 30t-1.5 22.5t-9 17.5t-13.5 12.5t-21.5 10t-26 8.5t-33.5 10q-13 3 -19 5v57h425z" />
<glyph unicode="&#xe050;" d="M1300 900h-50q0 21 -4 37t-9.5 26.5t-18 17.5t-22 11t-28.5 5.5t-31 2t-37 0.5h-200v-850q0 -22 25 -34.5t50 -13.5l25 -2v-100h-400v100q4 0 11 0.5t24 3t30 7t24 15t11 24.5v850h-200q-25 0 -37 -0.5t-31 -2t-28.5 -5.5t-22 -11t-18 -17.5t-9.5 -26.5t-4 -37h-50v300 h1000v-300zM175 1000h-75v-800h75l-125 -167l-125 167h75v800h-75l125 167z" />
<glyph unicode="&#xe051;" d="M1100 900h-50q0 21 -4 37t-9.5 26.5t-18 17.5t-22 11t-28.5 5.5t-31 2t-37 0.5h-200v-650q0 -22 25 -34.5t50 -13.5l25 -2v-100h-400v100q4 0 11 0.5t24 3t30 7t24 15t11 24.5v650h-200q-25 0 -37 -0.5t-31 -2t-28.5 -5.5t-22 -11t-18 -17.5t-9.5 -26.5t-4 -37h-50v300 h1000v-300zM1167 50l-167 -125v75h-800v-75l-167 125l167 125v-75h800v75z" />
<glyph unicode="&#xe052;" d="M50 1100h600q21 0 35.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-600q-21 0 -35.5 14.5t-14.5 35.5v100q0 21 14.5 35.5t35.5 14.5zM50 800h1000q21 0 35.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-1000q-21 0 -35.5 14.5t-14.5 35.5v100 q0 21 14.5 35.5t35.5 14.5zM50 500h800q21 0 35.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-800q-21 0 -35.5 14.5t-14.5 35.5v100q0 21 14.5 35.5t35.5 14.5zM50 200h1100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-1100 q-21 0 -35.5 14.5t-14.5 35.5v100q0 21 14.5 35.5t35.5 14.5z" />
<glyph unicode="&#xe053;" d="M250 1100h700q21 0 35.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-700q-21 0 -35.5 14.5t-14.5 35.5v100q0 21 14.5 35.5t35.5 14.5zM50 800h1100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-1100q-21 0 -35.5 14.5t-14.5 35.5v100 q0 21 14.5 35.5t35.5 14.5zM250 500h700q21 0 35.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-700q-21 0 -35.5 14.5t-14.5 35.5v100q0 21 14.5 35.5t35.5 14.5zM50 200h1100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-1100 q-21 0 -35.5 14.5t-14.5 35.5v100q0 21 14.5 35.5t35.5 14.5z" />
<glyph unicode="&#xe054;" d="M500 950v100q0 21 14.5 35.5t35.5 14.5h600q21 0 35.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-600q-21 0 -35.5 14.5t-14.5 35.5zM100 650v100q0 21 14.5 35.5t35.5 14.5h1000q21 0 35.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-1000 q-21 0 -35.5 14.5t-14.5 35.5zM300 350v100q0 21 14.5 35.5t35.5 14.5h800q21 0 35.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-800q-21 0 -35.5 14.5t-14.5 35.5zM0 50v100q0 21 14.5 35.5t35.5 14.5h1100q21 0 35.5 -14.5t14.5 -35.5v-100 q0 -21 -14.5 -35.5t-35.5 -14.5h-1100q-21 0 -35.5 14.5t-14.5 35.5z" />
<glyph unicode="&#xe055;" d="M50 1100h1100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-1100q-21 0 -35.5 14.5t-14.5 35.5v100q0 21 14.5 35.5t35.5 14.5zM50 800h1100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-1100q-21 0 -35.5 14.5t-14.5 35.5v100 q0 21 14.5 35.5t35.5 14.5zM50 500h1100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-1100q-21 0 -35.5 14.5t-14.5 35.5v100q0 21 14.5 35.5t35.5 14.5zM50 200h1100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-1100 q-21 0 -35.5 14.5t-14.5 35.5v100q0 21 14.5 35.5t35.5 14.5z" />
<glyph unicode="&#xe056;" d="M50 1100h100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-100q-21 0 -35.5 14.5t-14.5 35.5v100q0 21 14.5 35.5t35.5 14.5zM350 1100h800q21 0 35.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-800q-21 0 -35.5 14.5t-14.5 35.5v100 q0 21 14.5 35.5t35.5 14.5zM50 800h100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-100q-21 0 -35.5 14.5t-14.5 35.5v100q0 21 14.5 35.5t35.5 14.5zM350 800h800q21 0 35.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-800 q-21 0 -35.5 14.5t-14.5 35.5v100q0 21 14.5 35.5t35.5 14.5zM50 500h100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-100q-21 0 -35.5 14.5t-14.5 35.5v100q0 21 14.5 35.5t35.5 14.5zM350 500h800q21 0 35.5 -14.5t14.5 -35.5v-100 q0 -21 -14.5 -35.5t-35.5 -14.5h-800q-21 0 -35.5 14.5t-14.5 35.5v100q0 21 14.5 35.5t35.5 14.5zM50 200h100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-100q-21 0 -35.5 14.5t-14.5 35.5v100q0 21 14.5 35.5t35.5 14.5zM350 200h800 q21 0 35.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-800q-21 0 -35.5 14.5t-14.5 35.5v100q0 21 14.5 35.5t35.5 14.5z" />
<glyph unicode="&#xe057;" d="M400 0h-100v1100h100v-1100zM550 1100h100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-100q-21 0 -35.5 14.5t-14.5 35.5v100q0 21 14.5 35.5t35.5 14.5zM550 800h500q21 0 35.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-500 q-21 0 -35.5 14.5t-14.5 35.5v100q0 21 14.5 35.5t35.5 14.5zM267 550l-167 -125v75h-200v100h200v75zM550 500h300q21 0 35.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-300q-21 0 -35.5 14.5t-14.5 35.5v100q0 21 14.5 35.5t35.5 14.5zM550 200h600 q21 0 35.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-600q-21 0 -35.5 14.5t-14.5 35.5v100q0 21 14.5 35.5t35.5 14.5z" />
<glyph unicode="&#xe058;" d="M50 1100h100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-100q-21 0 -35.5 14.5t-14.5 35.5v100q0 21 14.5 35.5t35.5 14.5zM900 0h-100v1100h100v-1100zM50 800h500q21 0 35.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-500 q-21 0 -35.5 14.5t-14.5 35.5v100q0 21 14.5 35.5t35.5 14.5zM1100 600h200v-100h-200v-75l-167 125l167 125v-75zM50 500h300q21 0 35.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-300q-21 0 -35.5 14.5t-14.5 35.5v100q0 21 14.5 35.5t35.5 14.5zM50 200h600 q21 0 35.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-600q-21 0 -35.5 14.5t-14.5 35.5v100q0 21 14.5 35.5t35.5 14.5z" />
<glyph unicode="&#xe059;" d="M75 1000h750q31 0 53 -22t22 -53v-650q0 -31 -22 -53t-53 -22h-750q-31 0 -53 22t-22 53v650q0 31 22 53t53 22zM1200 300l-300 300l300 300v-600z" />
<glyph unicode="&#xe060;" d="M44 1100h1112q18 0 31 -13t13 -31v-1012q0 -18 -13 -31t-31 -13h-1112q-18 0 -31 13t-13 31v1012q0 18 13 31t31 13zM100 1000v-737l247 182l298 -131l-74 156l293 318l236 -288v500h-1000zM342 884q56 0 95 -39t39 -94.5t-39 -95t-95 -39.5t-95 39.5t-39 95t39 94.5 t95 39z" />
<glyph unicode="&#xe062;" d="M648 1169q117 0 216 -60t156.5 -161t57.5 -218q0 -115 -70 -258q-69 -109 -158 -225.5t-143 -179.5l-54 -62q-9 8 -25.5 24.5t-63.5 67.5t-91 103t-98.5 128t-95.5 148q-60 132 -60 249q0 88 34 169.5t91.5 142t137 96.5t166.5 36zM652.5 974q-91.5 0 -156.5 -65 t-65 -157t65 -156.5t156.5 -64.5t156.5 64.5t65 156.5t-65 157t-156.5 65z" />
<glyph unicode="&#xe063;" d="M600 1177q117 0 224 -45.5t184.5 -123t123 -184.5t45.5 -224t-45.5 -224t-123 -184.5t-184.5 -123t-224 -45.5t-224 45.5t-184.5 123t-123 184.5t-45.5 224t45.5 224t123 184.5t184.5 123t224 45.5zM600 173v854q-116 0 -214.5 -57t-155.5 -155.5t-57 -214.5t57 -214.5 t155.5 -155.5t214.5 -57z" />
<glyph unicode="&#xe064;" d="M554 1295q21 -72 57.5 -143.5t76 -130t83 -118t82.5 -117t70 -116t49.5 -126t18.5 -136.5q0 -71 -25.5 -135t-68.5 -111t-99 -82t-118.5 -54t-125.5 -23q-84 5 -161.5 34t-139.5 78.5t-99 125t-37 164.5q0 69 18 136.5t49.5 126.5t69.5 116.5t81.5 117.5t83.5 119 t76.5 131t58.5 143zM344 710q-23 -33 -43.5 -70.5t-40.5 -102.5t-17 -123q1 -37 14.5 -69.5t30 -52t41 -37t38.5 -24.5t33 -15q21 -7 32 -1t13 22l6 34q2 10 -2.5 22t-13.5 19q-5 4 -14 12t-29.5 40.5t-32.5 73.5q-26 89 6 271q2 11 -6 11q-8 1 -15 -10z" />
<glyph unicode="&#xe065;" d="M1000 1013l108 115q2 1 5 2t13 2t20.5 -1t25 -9.5t28.5 -21.5q22 -22 27 -43t0 -32l-6 -10l-108 -115zM350 1100h400q50 0 105 -13l-187 -187h-368q-41 0 -70.5 -29.5t-29.5 -70.5v-500q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5v182l200 200v-332 q0 -165 -93.5 -257.5t-256.5 -92.5h-400q-165 0 -257.5 92.5t-92.5 257.5v400q0 165 92.5 257.5t257.5 92.5zM1009 803l-362 -362l-161 -50l55 170l355 355z" />
<glyph unicode="&#xe066;" d="M350 1100h361q-164 -146 -216 -200h-195q-41 0 -70.5 -29.5t-29.5 -70.5v-500q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5l200 153v-103q0 -165 -92.5 -257.5t-257.5 -92.5h-400q-165 0 -257.5 92.5t-92.5 257.5v400q0 165 92.5 257.5t257.5 92.5z M824 1073l339 -301q8 -7 8 -17.5t-8 -17.5l-340 -306q-7 -6 -12.5 -4t-6.5 11v203q-26 1 -54.5 0t-78.5 -7.5t-92 -17.5t-86 -35t-70 -57q10 59 33 108t51.5 81.5t65 58.5t68.5 40.5t67 24.5t56 13.5t40 4.5v210q1 10 6.5 12.5t13.5 -4.5z" />
<glyph unicode="&#xe067;" d="M350 1100h350q60 0 127 -23l-178 -177h-349q-41 0 -70.5 -29.5t-29.5 -70.5v-500q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5v69l200 200v-219q0 -165 -92.5 -257.5t-257.5 -92.5h-400q-165 0 -257.5 92.5t-92.5 257.5v400q0 165 92.5 257.5t257.5 92.5z M643 639l395 395q7 7 17.5 7t17.5 -7l101 -101q7 -7 7 -17.5t-7 -17.5l-531 -532q-7 -7 -17.5 -7t-17.5 7l-248 248q-7 7 -7 17.5t7 17.5l101 101q7 7 17.5 7t17.5 -7l111 -111q8 -7 18 -7t18 7z" />
<glyph unicode="&#xe068;" d="M318 918l264 264q8 8 18 8t18 -8l260 -264q7 -8 4.5 -13t-12.5 -5h-170v-200h200v173q0 10 5 12t13 -5l264 -260q8 -7 8 -17.5t-8 -17.5l-264 -265q-8 -7 -13 -5t-5 12v173h-200v-200h170q10 0 12.5 -5t-4.5 -13l-260 -264q-8 -8 -18 -8t-18 8l-264 264q-8 8 -5.5 13 t12.5 5h175v200h-200v-173q0 -10 -5 -12t-13 5l-264 265q-8 7 -8 17.5t8 17.5l264 260q8 7 13 5t5 -12v-173h200v200h-175q-10 0 -12.5 5t5.5 13z" />
<glyph unicode="&#xe069;" d="M250 1100h100q21 0 35.5 -14.5t14.5 -35.5v-438l464 453q15 14 25.5 10t10.5 -25v-1000q0 -21 -10.5 -25t-25.5 10l-464 453v-438q0 -21 -14.5 -35.5t-35.5 -14.5h-100q-21 0 -35.5 14.5t-14.5 35.5v1000q0 21 14.5 35.5t35.5 14.5z" />
<glyph unicode="&#xe070;" d="M50 1100h100q21 0 35.5 -14.5t14.5 -35.5v-438l464 453q15 14 25.5 10t10.5 -25v-438l464 453q15 14 25.5 10t10.5 -25v-1000q0 -21 -10.5 -25t-25.5 10l-464 453v-438q0 -21 -10.5 -25t-25.5 10l-464 453v-438q0 -21 -14.5 -35.5t-35.5 -14.5h-100q-21 0 -35.5 14.5 t-14.5 35.5v1000q0 21 14.5 35.5t35.5 14.5z" />
<glyph unicode="&#xe071;" d="M1200 1050v-1000q0 -21 -10.5 -25t-25.5 10l-464 453v-438q0 -21 -10.5 -25t-25.5 10l-492 480q-15 14 -15 35t15 35l492 480q15 14 25.5 10t10.5 -25v-438l464 453q15 14 25.5 10t10.5 -25z" />
<glyph unicode="&#xe072;" d="M243 1074l814 -498q18 -11 18 -26t-18 -26l-814 -498q-18 -11 -30.5 -4t-12.5 28v1000q0 21 12.5 28t30.5 -4z" />
<glyph unicode="&#xe073;" d="M250 1000h200q21 0 35.5 -14.5t14.5 -35.5v-800q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5v800q0 21 14.5 35.5t35.5 14.5zM650 1000h200q21 0 35.5 -14.5t14.5 -35.5v-800q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5v800 q0 21 14.5 35.5t35.5 14.5z" />
<glyph unicode="&#xe074;" d="M1100 950v-800q0 -21 -14.5 -35.5t-35.5 -14.5h-800q-21 0 -35.5 14.5t-14.5 35.5v800q0 21 14.5 35.5t35.5 14.5h800q21 0 35.5 -14.5t14.5 -35.5z" />
<glyph unicode="&#xe075;" d="M500 612v438q0 21 10.5 25t25.5 -10l492 -480q15 -14 15 -35t-15 -35l-492 -480q-15 -14 -25.5 -10t-10.5 25v438l-464 -453q-15 -14 -25.5 -10t-10.5 25v1000q0 21 10.5 25t25.5 -10z" />
<glyph unicode="&#xe076;" d="M1048 1102l100 1q20 0 35 -14.5t15 -35.5l5 -1000q0 -21 -14.5 -35.5t-35.5 -14.5l-100 -1q-21 0 -35.5 14.5t-14.5 35.5l-2 437l-463 -454q-14 -15 -24.5 -10.5t-10.5 25.5l-2 437l-462 -455q-15 -14 -25.5 -9.5t-10.5 24.5l-5 1000q0 21 10.5 25.5t25.5 -10.5l466 -450 l-2 438q0 20 10.5 24.5t25.5 -9.5l466 -451l-2 438q0 21 14.5 35.5t35.5 14.5z" />
<glyph unicode="&#xe077;" d="M850 1100h100q21 0 35.5 -14.5t14.5 -35.5v-1000q0 -21 -14.5 -35.5t-35.5 -14.5h-100q-21 0 -35.5 14.5t-14.5 35.5v438l-464 -453q-15 -14 -25.5 -10t-10.5 25v1000q0 21 10.5 25t25.5 -10l464 -453v438q0 21 14.5 35.5t35.5 14.5z" />
<glyph unicode="&#xe078;" d="M686 1081l501 -540q15 -15 10.5 -26t-26.5 -11h-1042q-22 0 -26.5 11t10.5 26l501 540q15 15 36 15t36 -15zM150 400h1000q21 0 35.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-1000q-21 0 -35.5 14.5t-14.5 35.5v100q0 21 14.5 35.5t35.5 14.5z" />
<glyph unicode="&#xe079;" d="M885 900l-352 -353l352 -353l-197 -198l-552 552l552 550z" />
<glyph unicode="&#xe080;" d="M1064 547l-551 -551l-198 198l353 353l-353 353l198 198z" />
<glyph unicode="&#xe081;" d="M600 1177q117 0 224 -45.5t184.5 -123t123 -184.5t45.5 -224t-45.5 -224t-123 -184.5t-184.5 -123t-224 -45.5t-224 45.5t-184.5 123t-123 184.5t-45.5 224t45.5 224t123 184.5t184.5 123t224 45.5zM650 900h-100q-21 0 -35.5 -14.5t-14.5 -35.5v-150h-150 q-21 0 -35.5 -14.5t-14.5 -35.5v-100q0 -21 14.5 -35.5t35.5 -14.5h150v-150q0 -21 14.5 -35.5t35.5 -14.5h100q21 0 35.5 14.5t14.5 35.5v150h150q21 0 35.5 14.5t14.5 35.5v100q0 21 -14.5 35.5t-35.5 14.5h-150v150q0 21 -14.5 35.5t-35.5 14.5z" />
<glyph unicode="&#xe082;" d="M600 1177q117 0 224 -45.5t184.5 -123t123 -184.5t45.5 -224t-45.5 -224t-123 -184.5t-184.5 -123t-224 -45.5t-224 45.5t-184.5 123t-123 184.5t-45.5 224t45.5 224t123 184.5t184.5 123t224 45.5zM850 700h-500q-21 0 -35.5 -14.5t-14.5 -35.5v-100q0 -21 14.5 -35.5 t35.5 -14.5h500q21 0 35.5 14.5t14.5 35.5v100q0 21 -14.5 35.5t-35.5 14.5z" />
<glyph unicode="&#xe083;" d="M600 1177q117 0 224 -45.5t184.5 -123t123 -184.5t45.5 -224t-45.5 -224t-123 -184.5t-184.5 -123t-224 -45.5t-224 45.5t-184.5 123t-123 184.5t-45.5 224t45.5 224t123 184.5t184.5 123t224 45.5zM741.5 913q-12.5 0 -21.5 -9l-120 -120l-120 120q-9 9 -21.5 9 t-21.5 -9l-141 -141q-9 -9 -9 -21.5t9 -21.5l120 -120l-120 -120q-9 -9 -9 -21.5t9 -21.5l141 -141q9 -9 21.5 -9t21.5 9l120 120l120 -120q9 -9 21.5 -9t21.5 9l141 141q9 9 9 21.5t-9 21.5l-120 120l120 120q9 9 9 21.5t-9 21.5l-141 141q-9 9 -21.5 9z" />
<glyph unicode="&#xe084;" d="M600 1177q117 0 224 -45.5t184.5 -123t123 -184.5t45.5 -224t-45.5 -224t-123 -184.5t-184.5 -123t-224 -45.5t-224 45.5t-184.5 123t-123 184.5t-45.5 224t45.5 224t123 184.5t184.5 123t224 45.5zM546 623l-84 85q-7 7 -17.5 7t-18.5 -7l-139 -139q-7 -8 -7 -18t7 -18 l242 -241q7 -8 17.5 -8t17.5 8l375 375q7 7 7 17.5t-7 18.5l-139 139q-7 7 -17.5 7t-17.5 -7z" />
<glyph unicode="&#xe085;" d="M600 1177q117 0 224 -45.5t184.5 -123t123 -184.5t45.5 -224t-45.5 -224t-123 -184.5t-184.5 -123t-224 -45.5t-224 45.5t-184.5 123t-123 184.5t-45.5 224t45.5 224t123 184.5t184.5 123t224 45.5zM588 941q-29 0 -59 -5.5t-63 -20.5t-58 -38.5t-41.5 -63t-16.5 -89.5 q0 -25 20 -25h131q30 -5 35 11q6 20 20.5 28t45.5 8q20 0 31.5 -10.5t11.5 -28.5q0 -23 -7 -34t-26 -18q-1 0 -13.5 -4t-19.5 -7.5t-20 -10.5t-22 -17t-18.5 -24t-15.5 -35t-8 -46q-1 -8 5.5 -16.5t20.5 -8.5h173q7 0 22 8t35 28t37.5 48t29.5 74t12 100q0 47 -17 83 t-42.5 57t-59.5 34.5t-64 18t-59 4.5zM675 400h-150q-10 0 -17.5 -7.5t-7.5 -17.5v-150q0 -10 7.5 -17.5t17.5 -7.5h150q10 0 17.5 7.5t7.5 17.5v150q0 10 -7.5 17.5t-17.5 7.5z" />
<glyph unicode="&#xe086;" d="M600 1177q117 0 224 -45.5t184.5 -123t123 -184.5t45.5 -224t-45.5 -224t-123 -184.5t-184.5 -123t-224 -45.5t-224 45.5t-184.5 123t-123 184.5t-45.5 224t45.5 224t123 184.5t184.5 123t224 45.5zM675 1000h-150q-10 0 -17.5 -7.5t-7.5 -17.5v-150q0 -10 7.5 -17.5 t17.5 -7.5h150q10 0 17.5 7.5t7.5 17.5v150q0 10 -7.5 17.5t-17.5 7.5zM675 700h-250q-10 0 -17.5 -7.5t-7.5 -17.5v-50q0 -10 7.5 -17.5t17.5 -7.5h75v-200h-75q-10 0 -17.5 -7.5t-7.5 -17.5v-50q0 -10 7.5 -17.5t17.5 -7.5h350q10 0 17.5 7.5t7.5 17.5v50q0 10 -7.5 17.5 t-17.5 7.5h-75v275q0 10 -7.5 17.5t-17.5 7.5z" />
<glyph unicode="&#xe087;" d="M525 1200h150q10 0 17.5 -7.5t7.5 -17.5v-194q103 -27 178.5 -102.5t102.5 -178.5h194q10 0 17.5 -7.5t7.5 -17.5v-150q0 -10 -7.5 -17.5t-17.5 -7.5h-194q-27 -103 -102.5 -178.5t-178.5 -102.5v-194q0 -10 -7.5 -17.5t-17.5 -7.5h-150q-10 0 -17.5 7.5t-7.5 17.5v194 q-103 27 -178.5 102.5t-102.5 178.5h-194q-10 0 -17.5 7.5t-7.5 17.5v150q0 10 7.5 17.5t17.5 7.5h194q27 103 102.5 178.5t178.5 102.5v194q0 10 7.5 17.5t17.5 7.5zM700 893v-168q0 -10 -7.5 -17.5t-17.5 -7.5h-150q-10 0 -17.5 7.5t-7.5 17.5v168q-68 -23 -119 -74 t-74 -119h168q10 0 17.5 -7.5t7.5 -17.5v-150q0 -10 -7.5 -17.5t-17.5 -7.5h-168q23 -68 74 -119t119 -74v168q0 10 7.5 17.5t17.5 7.5h150q10 0 17.5 -7.5t7.5 -17.5v-168q68 23 119 74t74 119h-168q-10 0 -17.5 7.5t-7.5 17.5v150q0 10 7.5 17.5t17.5 7.5h168 q-23 68 -74 119t-119 74z" />
<glyph unicode="&#xe088;" d="M600 1177q117 0 224 -45.5t184.5 -123t123 -184.5t45.5 -224t-45.5 -224t-123 -184.5t-184.5 -123t-224 -45.5t-224 45.5t-184.5 123t-123 184.5t-45.5 224t45.5 224t123 184.5t184.5 123t224 45.5zM600 1027q-116 0 -214.5 -57t-155.5 -155.5t-57 -214.5t57 -214.5 t155.5 -155.5t214.5 -57t214.5 57t155.5 155.5t57 214.5t-57 214.5t-155.5 155.5t-214.5 57zM759 823l64 -64q7 -7 7 -17.5t-7 -17.5l-124 -124l124 -124q7 -7 7 -17.5t-7 -17.5l-64 -64q-7 -7 -17.5 -7t-17.5 7l-124 124l-124 -124q-7 -7 -17.5 -7t-17.5 7l-64 64 q-7 7 -7 17.5t7 17.5l124 124l-124 124q-7 7 -7 17.5t7 17.5l64 64q7 7 17.5 7t17.5 -7l124 -124l124 124q7 7 17.5 7t17.5 -7z" />
<glyph unicode="&#xe089;" d="M600 1177q117 0 224 -45.5t184.5 -123t123 -184.5t45.5 -224t-45.5 -224t-123 -184.5t-184.5 -123t-224 -45.5t-224 45.5t-184.5 123t-123 184.5t-45.5 224t45.5 224t123 184.5t184.5 123t224 45.5zM600 1027q-116 0 -214.5 -57t-155.5 -155.5t-57 -214.5t57 -214.5 t155.5 -155.5t214.5 -57t214.5 57t155.5 155.5t57 214.5t-57 214.5t-155.5 155.5t-214.5 57zM782 788l106 -106q7 -7 7 -17.5t-7 -17.5l-320 -321q-8 -7 -18 -7t-18 7l-202 203q-8 7 -8 17.5t8 17.5l106 106q7 8 17.5 8t17.5 -8l79 -79l197 197q7 7 17.5 7t17.5 -7z" />
<glyph unicode="&#xe090;" d="M600 1177q117 0 224 -45.5t184.5 -123t123 -184.5t45.5 -224t-45.5 -224t-123 -184.5t-184.5 -123t-224 -45.5t-224 45.5t-184.5 123t-123 184.5t-45.5 224t45.5 224t123 184.5t184.5 123t224 45.5zM600 1027q-116 0 -214.5 -57t-155.5 -155.5t-57 -214.5q0 -120 65 -225 l587 587q-105 65 -225 65zM965 819l-584 -584q104 -62 219 -62q116 0 214.5 57t155.5 155.5t57 214.5q0 115 -62 219z" />
<glyph unicode="&#xe091;" d="M39 582l522 427q16 13 27.5 8t11.5 -26v-291h550q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-550v-291q0 -21 -11.5 -26t-27.5 8l-522 427q-16 13 -16 32t16 32z" />
<glyph unicode="&#xe092;" d="M639 1009l522 -427q16 -13 16 -32t-16 -32l-522 -427q-16 -13 -27.5 -8t-11.5 26v291h-550q-21 0 -35.5 14.5t-14.5 35.5v200q0 21 14.5 35.5t35.5 14.5h550v291q0 21 11.5 26t27.5 -8z" />
<glyph unicode="&#xe093;" d="M682 1161l427 -522q13 -16 8 -27.5t-26 -11.5h-291v-550q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5v550h-291q-21 0 -26 11.5t8 27.5l427 522q13 16 32 16t32 -16z" />
<glyph unicode="&#xe094;" d="M550 1200h200q21 0 35.5 -14.5t14.5 -35.5v-550h291q21 0 26 -11.5t-8 -27.5l-427 -522q-13 -16 -32 -16t-32 16l-427 522q-13 16 -8 27.5t26 11.5h291v550q0 21 14.5 35.5t35.5 14.5z" />
<glyph unicode="&#xe095;" d="M639 1109l522 -427q16 -13 16 -32t-16 -32l-522 -427q-16 -13 -27.5 -8t-11.5 26v291q-94 -2 -182 -20t-170.5 -52t-147 -92.5t-100.5 -135.5q5 105 27 193.5t67.5 167t113 135t167 91.5t225.5 42v262q0 21 11.5 26t27.5 -8z" />
<glyph unicode="&#xe096;" d="M850 1200h300q21 0 35.5 -14.5t14.5 -35.5v-300q0 -21 -10.5 -25t-24.5 10l-94 94l-249 -249q-8 -7 -18 -7t-18 7l-106 106q-7 8 -7 18t7 18l249 249l-94 94q-14 14 -10 24.5t25 10.5zM350 0h-300q-21 0 -35.5 14.5t-14.5 35.5v300q0 21 10.5 25t24.5 -10l94 -94l249 249 q8 7 18 7t18 -7l106 -106q7 -8 7 -18t-7 -18l-249 -249l94 -94q14 -14 10 -24.5t-25 -10.5z" />
<glyph unicode="&#xe097;" d="M1014 1120l106 -106q7 -8 7 -18t-7 -18l-249 -249l94 -94q14 -14 10 -24.5t-25 -10.5h-300q-21 0 -35.5 14.5t-14.5 35.5v300q0 21 10.5 25t24.5 -10l94 -94l249 249q8 7 18 7t18 -7zM250 600h300q21 0 35.5 -14.5t14.5 -35.5v-300q0 -21 -10.5 -25t-24.5 10l-94 94 l-249 -249q-8 -7 -18 -7t-18 7l-106 106q-7 8 -7 18t7 18l249 249l-94 94q-14 14 -10 24.5t25 10.5z" />
<glyph unicode="&#xe101;" d="M600 1177q117 0 224 -45.5t184.5 -123t123 -184.5t45.5 -224t-45.5 -224t-123 -184.5t-184.5 -123t-224 -45.5t-224 45.5t-184.5 123t-123 184.5t-45.5 224t45.5 224t123 184.5t184.5 123t224 45.5zM704 900h-208q-20 0 -32 -14.5t-8 -34.5l58 -302q4 -20 21.5 -34.5 t37.5 -14.5h54q20 0 37.5 14.5t21.5 34.5l58 302q4 20 -8 34.5t-32 14.5zM675 400h-150q-10 0 -17.5 -7.5t-7.5 -17.5v-150q0 -10 7.5 -17.5t17.5 -7.5h150q10 0 17.5 7.5t7.5 17.5v150q0 10 -7.5 17.5t-17.5 7.5z" />
<glyph unicode="&#xe102;" d="M260 1200q9 0 19 -2t15 -4l5 -2q22 -10 44 -23l196 -118q21 -13 36 -24q29 -21 37 -12q11 13 49 35l196 118q22 13 45 23q17 7 38 7q23 0 47 -16.5t37 -33.5l13 -16q14 -21 18 -45l25 -123l8 -44q1 -9 8.5 -14.5t17.5 -5.5h61q10 0 17.5 -7.5t7.5 -17.5v-50 q0 -10 -7.5 -17.5t-17.5 -7.5h-50q-10 0 -17.5 -7.5t-7.5 -17.5v-175h-400v300h-200v-300h-400v175q0 10 -7.5 17.5t-17.5 7.5h-50q-10 0 -17.5 7.5t-7.5 17.5v50q0 10 7.5 17.5t17.5 7.5h61q11 0 18 3t7 8q0 4 9 52l25 128q5 25 19 45q2 3 5 7t13.5 15t21.5 19.5t26.5 15.5 t29.5 7zM915 1079l-166 -162q-7 -7 -5 -12t12 -5h219q10 0 15 7t2 17l-51 149q-3 10 -11 12t-15 -6zM463 917l-177 157q-8 7 -16 5t-11 -12l-51 -143q-3 -10 2 -17t15 -7h231q11 0 12.5 5t-5.5 12zM500 0h-375q-10 0 -17.5 7.5t-7.5 17.5v375h400v-400zM1100 400v-375 q0 -10 -7.5 -17.5t-17.5 -7.5h-375v400h400z" />
<glyph unicode="&#xe103;" d="M1165 1190q8 3 21 -6.5t13 -17.5q-2 -178 -24.5 -323.5t-55.5 -245.5t-87 -174.5t-102.5 -118.5t-118 -68.5t-118.5 -33t-120 -4.5t-105 9.5t-90 16.5q-61 12 -78 11q-4 1 -12.5 0t-34 -14.5t-52.5 -40.5l-153 -153q-26 -24 -37 -14.5t-11 43.5q0 64 42 102q8 8 50.5 45 t66.5 58q19 17 35 47t13 61q-9 55 -10 102.5t7 111t37 130t78 129.5q39 51 80 88t89.5 63.5t94.5 45t113.5 36t129 31t157.5 37t182 47.5zM1116 1098q-8 9 -22.5 -3t-45.5 -50q-38 -47 -119 -103.5t-142 -89.5l-62 -33q-56 -30 -102 -57t-104 -68t-102.5 -80.5t-85.5 -91 t-64 -104.5q-24 -56 -31 -86t2 -32t31.5 17.5t55.5 59.5q25 30 94 75.5t125.5 77.5t147.5 81q70 37 118.5 69t102 79.5t99 111t86.5 148.5q22 50 24 60t-6 19z" />
<glyph unicode="&#xe104;" d="M653 1231q-39 -67 -54.5 -131t-10.5 -114.5t24.5 -96.5t47.5 -80t63.5 -62.5t68.5 -46.5t65 -30q-4 7 -17.5 35t-18.5 39.5t-17 39.5t-17 43t-13 42t-9.5 44.5t-2 42t4 43t13.5 39t23 38.5q96 -42 165 -107.5t105 -138t52 -156t13 -159t-19 -149.5q-13 -55 -44 -106.5 t-68 -87t-78.5 -64.5t-72.5 -45t-53 -22q-72 -22 -127 -11q-31 6 -13 19q6 3 17 7q13 5 32.5 21t41 44t38.5 63.5t21.5 81.5t-6.5 94.5t-50 107t-104 115.5q10 -104 -0.5 -189t-37 -140.5t-65 -93t-84 -52t-93.5 -11t-95 24.5q-80 36 -131.5 114t-53.5 171q-2 23 0 49.5 t4.5 52.5t13.5 56t27.5 60t46 64.5t69.5 68.5q-8 -53 -5 -102.5t17.5 -90t34 -68.5t44.5 -39t49 -2q31 13 38.5 36t-4.5 55t-29 64.5t-36 75t-26 75.5q-15 85 2 161.5t53.5 128.5t85.5 92.5t93.5 61t81.5 25.5z" />
<glyph unicode="&#xe105;" d="M600 1094q82 0 160.5 -22.5t140 -59t116.5 -82.5t94.5 -95t68 -95t42.5 -82.5t14 -57.5t-14 -57.5t-43 -82.5t-68.5 -95t-94.5 -95t-116.5 -82.5t-140 -59t-159.5 -22.5t-159.5 22.5t-140 59t-116.5 82.5t-94.5 95t-68.5 95t-43 82.5t-14 57.5t14 57.5t42.5 82.5t68 95 t94.5 95t116.5 82.5t140 59t160.5 22.5zM888 829q-15 15 -18 12t5 -22q25 -57 25 -119q0 -124 -88 -212t-212 -88t-212 88t-88 212q0 59 23 114q8 19 4.5 22t-17.5 -12q-70 -69 -160 -184q-13 -16 -15 -40.5t9 -42.5q22 -36 47 -71t70 -82t92.5 -81t113 -58.5t133.5 -24.5 t133.5 24t113 58.5t92.5 81.5t70 81.5t47 70.5q11 18 9 42.5t-14 41.5q-90 117 -163 189zM448 727l-35 -36q-15 -15 -19.5 -38.5t4.5 -41.5q37 -68 93 -116q16 -13 38.5 -11t36.5 17l35 34q14 15 12.5 33.5t-16.5 33.5q-44 44 -89 117q-11 18 -28 20t-32 -12z" />
<glyph unicode="&#xe106;" d="M592 0h-148l31 120q-91 20 -175.5 68.5t-143.5 106.5t-103.5 119t-66.5 110t-22 76q0 21 14 57.5t42.5 82.5t68 95t94.5 95t116.5 82.5t140 59t160.5 22.5q61 0 126 -15l32 121h148zM944 770l47 181q108 -85 176.5 -192t68.5 -159q0 -26 -19.5 -71t-59.5 -102t-93 -112 t-129 -104.5t-158 -75.5l46 173q77 49 136 117t97 131q11 18 9 42.5t-14 41.5q-54 70 -107 130zM310 824q-70 -69 -160 -184q-13 -16 -15 -40.5t9 -42.5q18 -30 39 -60t57 -70.5t74 -73t90 -61t105 -41.5l41 154q-107 18 -178.5 101.5t-71.5 193.5q0 59 23 114q8 19 4.5 22 t-17.5 -12zM448 727l-35 -36q-15 -15 -19.5 -38.5t4.5 -41.5q37 -68 93 -116q16 -13 38.5 -11t36.5 17l12 11l22 86l-3 4q-44 44 -89 117q-11 18 -28 20t-32 -12z" />
<glyph unicode="&#xe107;" d="M-90 100l642 1066q20 31 48 28.5t48 -35.5l642 -1056q21 -32 7.5 -67.5t-50.5 -35.5h-1294q-37 0 -50.5 34t7.5 66zM155 200h345v75q0 10 7.5 17.5t17.5 7.5h150q10 0 17.5 -7.5t7.5 -17.5v-75h345l-445 723zM496 700h208q20 0 32 -14.5t8 -34.5l-58 -252 q-4 -20 -21.5 -34.5t-37.5 -14.5h-54q-20 0 -37.5 14.5t-21.5 34.5l-58 252q-4 20 8 34.5t32 14.5z" />
<glyph unicode="&#xe108;" d="M650 1200q62 0 106 -44t44 -106v-339l363 -325q15 -14 26 -38.5t11 -44.5v-41q0 -20 -12 -26.5t-29 5.5l-359 249v-263q100 -93 100 -113v-64q0 -21 -13 -29t-32 1l-205 128l-205 -128q-19 -9 -32 -1t-13 29v64q0 20 100 113v263l-359 -249q-17 -12 -29 -5.5t-12 26.5v41 q0 20 11 44.5t26 38.5l363 325v339q0 62 44 106t106 44z" />
<glyph unicode="&#xe109;" d="M850 1200h100q21 0 35.5 -14.5t14.5 -35.5v-50h50q21 0 35.5 -14.5t14.5 -35.5v-150h-1100v150q0 21 14.5 35.5t35.5 14.5h50v50q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-50h500v50q0 21 14.5 35.5t35.5 14.5zM1100 800v-750q0 -21 -14.5 -35.5 t-35.5 -14.5h-1000q-21 0 -35.5 14.5t-14.5 35.5v750h1100zM100 600v-100h100v100h-100zM300 600v-100h100v100h-100zM500 600v-100h100v100h-100zM700 600v-100h100v100h-100zM900 600v-100h100v100h-100zM100 400v-100h100v100h-100zM300 400v-100h100v100h-100zM500 400 v-100h100v100h-100zM700 400v-100h100v100h-100zM900 400v-100h100v100h-100zM100 200v-100h100v100h-100zM300 200v-100h100v100h-100zM500 200v-100h100v100h-100zM700 200v-100h100v100h-100zM900 200v-100h100v100h-100z" />
<glyph unicode="&#xe110;" d="M1135 1165l249 -230q15 -14 15 -35t-15 -35l-249 -230q-14 -14 -24.5 -10t-10.5 25v150h-159l-600 -600h-291q-21 0 -35.5 14.5t-14.5 35.5v100q0 21 14.5 35.5t35.5 14.5h209l600 600h241v150q0 21 10.5 25t24.5 -10zM522 819l-141 -141l-122 122h-209q-21 0 -35.5 14.5 t-14.5 35.5v100q0 21 14.5 35.5t35.5 14.5h291zM1135 565l249 -230q15 -14 15 -35t-15 -35l-249 -230q-14 -14 -24.5 -10t-10.5 25v150h-241l-181 181l141 141l122 -122h159v150q0 21 10.5 25t24.5 -10z" />
<glyph unicode="&#xe111;" d="M100 1100h1000q41 0 70.5 -29.5t29.5 -70.5v-600q0 -41 -29.5 -70.5t-70.5 -29.5h-596l-304 -300v300h-100q-41 0 -70.5 29.5t-29.5 70.5v600q0 41 29.5 70.5t70.5 29.5z" />
<glyph unicode="&#xe112;" d="M150 1200h200q21 0 35.5 -14.5t14.5 -35.5v-250h-300v250q0 21 14.5 35.5t35.5 14.5zM850 1200h200q21 0 35.5 -14.5t14.5 -35.5v-250h-300v250q0 21 14.5 35.5t35.5 14.5zM1100 800v-300q0 -41 -3 -77.5t-15 -89.5t-32 -96t-58 -89t-89 -77t-129 -51t-174 -20t-174 20 t-129 51t-89 77t-58 89t-32 96t-15 89.5t-3 77.5v300h300v-250v-27v-42.5t1.5 -41t5 -38t10 -35t16.5 -30t25.5 -24.5t35 -19t46.5 -12t60 -4t60 4.5t46.5 12.5t35 19.5t25 25.5t17 30.5t10 35t5 38t2 40.5t-0.5 42v25v250h300z" />
<glyph unicode="&#xe113;" d="M1100 411l-198 -199l-353 353l-353 -353l-197 199l551 551z" />
<glyph unicode="&#xe114;" d="M1101 789l-550 -551l-551 551l198 199l353 -353l353 353z" />
<glyph unicode="&#xe115;" d="M404 1000h746q21 0 35.5 -14.5t14.5 -35.5v-551h150q21 0 25 -10.5t-10 -24.5l-230 -249q-14 -15 -35 -15t-35 15l-230 249q-14 14 -10 24.5t25 10.5h150v401h-381zM135 984l230 -249q14 -14 10 -24.5t-25 -10.5h-150v-400h385l215 -200h-750q-21 0 -35.5 14.5 t-14.5 35.5v550h-150q-21 0 -25 10.5t10 24.5l230 249q14 15 35 15t35 -15z" />
<glyph unicode="&#xe116;" d="M56 1200h94q17 0 31 -11t18 -27l38 -162h896q24 0 39 -18.5t10 -42.5l-100 -475q-5 -21 -27 -42.5t-55 -21.5h-633l48 -200h535q21 0 35.5 -14.5t14.5 -35.5t-14.5 -35.5t-35.5 -14.5h-50v-50q0 -21 -14.5 -35.5t-35.5 -14.5t-35.5 14.5t-14.5 35.5v50h-300v-50 q0 -21 -14.5 -35.5t-35.5 -14.5t-35.5 14.5t-14.5 35.5v50h-31q-18 0 -32.5 10t-20.5 19l-5 10l-201 961h-54q-20 0 -35 14.5t-15 35.5t15 35.5t35 14.5z" />
<glyph unicode="&#xe117;" d="M1200 1000v-100h-1200v100h200q0 41 29.5 70.5t70.5 29.5h300q41 0 70.5 -29.5t29.5 -70.5h500zM0 800h1200v-800h-1200v800z" />
<glyph unicode="&#xe118;" d="M200 800l-200 -400v600h200q0 41 29.5 70.5t70.5 29.5h300q42 0 71 -29.5t29 -70.5h500v-200h-1000zM1500 700l-300 -700h-1200l300 700h1200z" />
<glyph unicode="&#xe119;" d="M635 1184l230 -249q14 -14 10 -24.5t-25 -10.5h-150v-601h150q21 0 25 -10.5t-10 -24.5l-230 -249q-14 -15 -35 -15t-35 15l-230 249q-14 14 -10 24.5t25 10.5h150v601h-150q-21 0 -25 10.5t10 24.5l230 249q14 15 35 15t35 -15z" />
<glyph unicode="&#xe120;" d="M936 864l249 -229q14 -15 14 -35.5t-14 -35.5l-249 -229q-15 -15 -25.5 -10.5t-10.5 24.5v151h-600v-151q0 -20 -10.5 -24.5t-25.5 10.5l-249 229q-14 15 -14 35.5t14 35.5l249 229q15 15 25.5 10.5t10.5 -25.5v-149h600v149q0 21 10.5 25.5t25.5 -10.5z" />
<glyph unicode="&#xe121;" d="M1169 400l-172 732q-5 23 -23 45.5t-38 22.5h-672q-20 0 -38 -20t-23 -41l-172 -739h1138zM1100 300h-1000q-41 0 -70.5 -29.5t-29.5 -70.5v-100q0 -41 29.5 -70.5t70.5 -29.5h1000q41 0 70.5 29.5t29.5 70.5v100q0 41 -29.5 70.5t-70.5 29.5zM800 100v100h100v-100h-100 zM1000 100v100h100v-100h-100z" />
<glyph unicode="&#xe122;" d="M1150 1100q21 0 35.5 -14.5t14.5 -35.5v-850q0 -21 -14.5 -35.5t-35.5 -14.5t-35.5 14.5t-14.5 35.5v850q0 21 14.5 35.5t35.5 14.5zM1000 200l-675 200h-38l47 -276q3 -16 -5.5 -20t-29.5 -4h-7h-84q-20 0 -34.5 14t-18.5 35q-55 337 -55 351v250v6q0 16 1 23.5t6.5 14 t17.5 6.5h200l675 250v-850zM0 750v-250q-4 0 -11 0.5t-24 6t-30 15t-24 30t-11 48.5v50q0 26 10.5 46t25 30t29 16t25.5 7z" />
<glyph unicode="&#xe123;" d="M553 1200h94q20 0 29 -10.5t3 -29.5l-18 -37q83 -19 144 -82.5t76 -140.5l63 -327l118 -173h17q19 0 33 -14.5t14 -35t-13 -40.5t-31 -27q-8 -4 -23 -9.5t-65 -19.5t-103 -25t-132.5 -20t-158.5 -9q-57 0 -115 5t-104 12t-88.5 15.5t-73.5 17.5t-54.5 16t-35.5 12l-11 4 q-18 8 -31 28t-13 40.5t14 35t33 14.5h17l118 173l63 327q15 77 76 140t144 83l-18 32q-6 19 3.5 32t28.5 13zM498 110q50 -6 102 -6q53 0 102 6q-12 -49 -39.5 -79.5t-62.5 -30.5t-63 30.5t-39 79.5z" />
<glyph unicode="&#xe124;" d="M800 946l224 78l-78 -224l234 -45l-180 -155l180 -155l-234 -45l78 -224l-224 78l-45 -234l-155 180l-155 -180l-45 234l-224 -78l78 224l-234 45l180 155l-180 155l234 45l-78 224l224 -78l45 234l155 -180l155 180z" />
<glyph unicode="&#xe125;" d="M650 1200h50q40 0 70 -40.5t30 -84.5v-150l-28 -125h328q40 0 70 -40.5t30 -84.5v-100q0 -45 -29 -74l-238 -344q-16 -24 -38 -40.5t-45 -16.5h-250q-7 0 -42 25t-66 50l-31 25h-61q-45 0 -72.5 18t-27.5 57v400q0 36 20 63l145 196l96 198q13 28 37.5 48t51.5 20z M650 1100l-100 -212l-150 -213v-375h100l136 -100h214l250 375v125h-450l50 225v175h-50zM50 800h100q21 0 35.5 -14.5t14.5 -35.5v-500q0 -21 -14.5 -35.5t-35.5 -14.5h-100q-21 0 -35.5 14.5t-14.5 35.5v500q0 21 14.5 35.5t35.5 14.5z" />
<glyph unicode="&#xe126;" d="M600 1100h250q23 0 45 -16.5t38 -40.5l238 -344q29 -29 29 -74v-100q0 -44 -30 -84.5t-70 -40.5h-328q28 -118 28 -125v-150q0 -44 -30 -84.5t-70 -40.5h-50q-27 0 -51.5 20t-37.5 48l-96 198l-145 196q-20 27 -20 63v400q0 39 27.5 57t72.5 18h61q124 100 139 100z M50 1000h100q21 0 35.5 -14.5t14.5 -35.5v-500q0 -21 -14.5 -35.5t-35.5 -14.5h-100q-21 0 -35.5 14.5t-14.5 35.5v500q0 21 14.5 35.5t35.5 14.5zM636 1000l-136 -100h-100v-375l150 -213l100 -212h50v175l-50 225h450v125l-250 375h-214z" />
<glyph unicode="&#xe127;" d="M356 873l363 230q31 16 53 -6l110 -112q13 -13 13.5 -32t-11.5 -34l-84 -121h302q84 0 138 -38t54 -110t-55 -111t-139 -39h-106l-131 -339q-6 -21 -19.5 -41t-28.5 -20h-342q-7 0 -90 81t-83 94v525q0 17 14 35.5t28 28.5zM400 792v-503l100 -89h293l131 339 q6 21 19.5 41t28.5 20h203q21 0 30.5 25t0.5 50t-31 25h-456h-7h-6h-5.5t-6 0.5t-5 1.5t-5 2t-4 2.5t-4 4t-2.5 4.5q-12 25 5 47l146 183l-86 83zM50 800h100q21 0 35.5 -14.5t14.5 -35.5v-500q0 -21 -14.5 -35.5t-35.5 -14.5h-100q-21 0 -35.5 14.5t-14.5 35.5v500 q0 21 14.5 35.5t35.5 14.5z" />
<glyph unicode="&#xe128;" d="M475 1103l366 -230q2 -1 6 -3.5t14 -10.5t18 -16.5t14.5 -20t6.5 -22.5v-525q0 -13 -86 -94t-93 -81h-342q-15 0 -28.5 20t-19.5 41l-131 339h-106q-85 0 -139.5 39t-54.5 111t54 110t138 38h302l-85 121q-11 15 -10.5 34t13.5 32l110 112q22 22 53 6zM370 945l146 -183 q17 -22 5 -47q-2 -2 -3.5 -4.5t-4 -4t-4 -2.5t-5 -2t-5 -1.5t-6 -0.5h-6h-6.5h-6h-475v-100h221q15 0 29 -20t20 -41l130 -339h294l106 89v503l-342 236zM1050 800h100q21 0 35.5 -14.5t14.5 -35.5v-500q0 -21 -14.5 -35.5t-35.5 -14.5h-100q-21 0 -35.5 14.5t-14.5 35.5 v500q0 21 14.5 35.5t35.5 14.5z" />
<glyph unicode="&#xe129;" d="M550 1294q72 0 111 -55t39 -139v-106l339 -131q21 -6 41 -19.5t20 -28.5v-342q0 -7 -81 -90t-94 -83h-525q-17 0 -35.5 14t-28.5 28l-9 14l-230 363q-16 31 6 53l112 110q13 13 32 13.5t34 -11.5l121 -84v302q0 84 38 138t110 54zM600 972v203q0 21 -25 30.5t-50 0.5 t-25 -31v-456v-7v-6v-5.5t-0.5 -6t-1.5 -5t-2 -5t-2.5 -4t-4 -4t-4.5 -2.5q-25 -12 -47 5l-183 146l-83 -86l236 -339h503l89 100v293l-339 131q-21 6 -41 19.5t-20 28.5zM450 200h500q21 0 35.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-500 q-21 0 -35.5 14.5t-14.5 35.5v100q0 21 14.5 35.5t35.5 14.5z" />
<glyph unicode="&#xe130;" d="M350 1100h500q21 0 35.5 14.5t14.5 35.5v100q0 21 -14.5 35.5t-35.5 14.5h-500q-21 0 -35.5 -14.5t-14.5 -35.5v-100q0 -21 14.5 -35.5t35.5 -14.5zM600 306v-106q0 -84 -39 -139t-111 -55t-110 54t-38 138v302l-121 -84q-15 -12 -34 -11.5t-32 13.5l-112 110 q-22 22 -6 53l230 363q1 2 3.5 6t10.5 13.5t16.5 17t20 13.5t22.5 6h525q13 0 94 -83t81 -90v-342q0 -15 -20 -28.5t-41 -19.5zM308 900l-236 -339l83 -86l183 146q22 17 47 5q2 -1 4.5 -2.5t4 -4t2.5 -4t2 -5t1.5 -5t0.5 -6v-5.5v-6v-7v-456q0 -22 25 -31t50 0.5t25 30.5 v203q0 15 20 28.5t41 19.5l339 131v293l-89 100h-503z" />
<glyph unicode="&#xe131;" d="M600 1178q118 0 225 -45.5t184.5 -123t123 -184.5t45.5 -225t-45.5 -225t-123 -184.5t-184.5 -123t-225 -45.5t-225 45.5t-184.5 123t-123 184.5t-45.5 225t45.5 225t123 184.5t184.5 123t225 45.5zM914 632l-275 223q-16 13 -27.5 8t-11.5 -26v-137h-275 q-10 0 -17.5 -7.5t-7.5 -17.5v-150q0 -10 7.5 -17.5t17.5 -7.5h275v-137q0 -21 11.5 -26t27.5 8l275 223q16 13 16 32t-16 32z" />
<glyph unicode="&#xe132;" d="M600 1178q118 0 225 -45.5t184.5 -123t123 -184.5t45.5 -225t-45.5 -225t-123 -184.5t-184.5 -123t-225 -45.5t-225 45.5t-184.5 123t-123 184.5t-45.5 225t45.5 225t123 184.5t184.5 123t225 45.5zM561 855l-275 -223q-16 -13 -16 -32t16 -32l275 -223q16 -13 27.5 -8 t11.5 26v137h275q10 0 17.5 7.5t7.5 17.5v150q0 10 -7.5 17.5t-17.5 7.5h-275v137q0 21 -11.5 26t-27.5 -8z" />
<glyph unicode="&#xe133;" d="M600 1178q118 0 225 -45.5t184.5 -123t123 -184.5t45.5 -225t-45.5 -225t-123 -184.5t-184.5 -123t-225 -45.5t-225 45.5t-184.5 123t-123 184.5t-45.5 225t45.5 225t123 184.5t184.5 123t225 45.5zM855 639l-223 275q-13 16 -32 16t-32 -16l-223 -275q-13 -16 -8 -27.5 t26 -11.5h137v-275q0 -10 7.5 -17.5t17.5 -7.5h150q10 0 17.5 7.5t7.5 17.5v275h137q21 0 26 11.5t-8 27.5z" />
<glyph unicode="&#xe134;" d="M600 1178q118 0 225 -45.5t184.5 -123t123 -184.5t45.5 -225t-45.5 -225t-123 -184.5t-184.5 -123t-225 -45.5t-225 45.5t-184.5 123t-123 184.5t-45.5 225t45.5 225t123 184.5t184.5 123t225 45.5zM675 900h-150q-10 0 -17.5 -7.5t-7.5 -17.5v-275h-137q-21 0 -26 -11.5 t8 -27.5l223 -275q13 -16 32 -16t32 16l223 275q13 16 8 27.5t-26 11.5h-137v275q0 10 -7.5 17.5t-17.5 7.5z" />
<glyph unicode="&#xe135;" d="M600 1176q116 0 222.5 -46t184 -123.5t123.5 -184t46 -222.5t-46 -222.5t-123.5 -184t-184 -123.5t-222.5 -46t-222.5 46t-184 123.5t-123.5 184t-46 222.5t46 222.5t123.5 184t184 123.5t222.5 46zM627 1101q-15 -12 -36.5 -20.5t-35.5 -12t-43 -8t-39 -6.5 q-15 -3 -45.5 0t-45.5 -2q-20 -7 -51.5 -26.5t-34.5 -34.5q-3 -11 6.5 -22.5t8.5 -18.5q-3 -34 -27.5 -91t-29.5 -79q-9 -34 5 -93t8 -87q0 -9 17 -44.5t16 -59.5q12 0 23 -5t23.5 -15t19.5 -14q16 -8 33 -15t40.5 -15t34.5 -12q21 -9 52.5 -32t60 -38t57.5 -11 q7 -15 -3 -34t-22.5 -40t-9.5 -38q13 -21 23 -34.5t27.5 -27.5t36.5 -18q0 -7 -3.5 -16t-3.5 -14t5 -17q104 -2 221 112q30 29 46.5 47t34.5 49t21 63q-13 8 -37 8.5t-36 7.5q-15 7 -49.5 15t-51.5 19q-18 0 -41 -0.5t-43 -1.5t-42 -6.5t-38 -16.5q-51 -35 -66 -12 q-4 1 -3.5 25.5t0.5 25.5q-6 13 -26.5 17.5t-24.5 6.5q1 15 -0.5 30.5t-7 28t-18.5 11.5t-31 -21q-23 -25 -42 4q-19 28 -8 58q6 16 22 22q6 -1 26 -1.5t33.5 -4t19.5 -13.5q7 -12 18 -24t21.5 -20.5t20 -15t15.5 -10.5l5 -3q2 12 7.5 30.5t8 34.5t-0.5 32q-3 18 3.5 29 t18 22.5t15.5 24.5q6 14 10.5 35t8 31t15.5 22.5t34 22.5q-6 18 10 36q8 0 24 -1.5t24.5 -1.5t20 4.5t20.5 15.5q-10 23 -31 42.5t-37.5 29.5t-49 27t-43.5 23q0 1 2 8t3 11.5t1.5 10.5t-1 9.5t-4.5 4.5q31 -13 58.5 -14.5t38.5 2.5l12 5q5 28 -9.5 46t-36.5 24t-50 15 t-41 20q-18 -4 -37 0zM613 994q0 -17 8 -42t17 -45t9 -23q-8 1 -39.5 5.5t-52.5 10t-37 16.5q3 11 16 29.5t16 25.5q10 -10 19 -10t14 6t13.5 14.5t16.5 12.5z" />
<glyph unicode="&#xe136;" d="M756 1157q164 92 306 -9l-259 -138l145 -232l251 126q6 -89 -34 -156.5t-117 -110.5q-60 -34 -127 -39.5t-126 16.5l-596 -596q-15 -16 -36.5 -16t-36.5 16l-111 110q-15 15 -15 36.5t15 37.5l600 599q-34 101 5.5 201.5t135.5 154.5z" />
<glyph unicode="&#xe137;" horiz-adv-x="1220" d="M100 1196h1000q41 0 70.5 -29.5t29.5 -70.5v-100q0 -41 -29.5 -70.5t-70.5 -29.5h-1000q-41 0 -70.5 29.5t-29.5 70.5v100q0 41 29.5 70.5t70.5 29.5zM1100 1096h-200v-100h200v100zM100 796h1000q41 0 70.5 -29.5t29.5 -70.5v-100q0 -41 -29.5 -70.5t-70.5 -29.5h-1000 q-41 0 -70.5 29.5t-29.5 70.5v100q0 41 29.5 70.5t70.5 29.5zM1100 696h-500v-100h500v100zM100 396h1000q41 0 70.5 -29.5t29.5 -70.5v-100q0 -41 -29.5 -70.5t-70.5 -29.5h-1000q-41 0 -70.5 29.5t-29.5 70.5v100q0 41 29.5 70.5t70.5 29.5zM1100 296h-300v-100h300v100z " />
<glyph unicode="&#xe138;" d="M150 1200h900q21 0 35.5 -14.5t14.5 -35.5t-14.5 -35.5t-35.5 -14.5h-900q-21 0 -35.5 14.5t-14.5 35.5t14.5 35.5t35.5 14.5zM700 500v-300l-200 -200v500l-350 500h900z" />
<glyph unicode="&#xe139;" d="M500 1200h200q41 0 70.5 -29.5t29.5 -70.5v-100h300q41 0 70.5 -29.5t29.5 -70.5v-400h-500v100h-200v-100h-500v400q0 41 29.5 70.5t70.5 29.5h300v100q0 41 29.5 70.5t70.5 29.5zM500 1100v-100h200v100h-200zM1200 400v-200q0 -41 -29.5 -70.5t-70.5 -29.5h-1000 q-41 0 -70.5 29.5t-29.5 70.5v200h1200z" />
<glyph unicode="&#xe140;" d="M50 1200h300q21 0 25 -10.5t-10 -24.5l-94 -94l199 -199q7 -8 7 -18t-7 -18l-106 -106q-8 -7 -18 -7t-18 7l-199 199l-94 -94q-14 -14 -24.5 -10t-10.5 25v300q0 21 14.5 35.5t35.5 14.5zM850 1200h300q21 0 35.5 -14.5t14.5 -35.5v-300q0 -21 -10.5 -25t-24.5 10l-94 94 l-199 -199q-8 -7 -18 -7t-18 7l-106 106q-7 8 -7 18t7 18l199 199l-94 94q-14 14 -10 24.5t25 10.5zM364 470l106 -106q7 -8 7 -18t-7 -18l-199 -199l94 -94q14 -14 10 -24.5t-25 -10.5h-300q-21 0 -35.5 14.5t-14.5 35.5v300q0 21 10.5 25t24.5 -10l94 -94l199 199 q8 7 18 7t18 -7zM1071 271l94 94q14 14 24.5 10t10.5 -25v-300q0 -21 -14.5 -35.5t-35.5 -14.5h-300q-21 0 -25 10.5t10 24.5l94 94l-199 199q-7 8 -7 18t7 18l106 106q8 7 18 7t18 -7z" />
<glyph unicode="&#xe141;" d="M596 1192q121 0 231.5 -47.5t190 -127t127 -190t47.5 -231.5t-47.5 -231.5t-127 -190.5t-190 -127t-231.5 -47t-231.5 47t-190.5 127t-127 190.5t-47 231.5t47 231.5t127 190t190.5 127t231.5 47.5zM596 1010q-112 0 -207.5 -55.5t-151 -151t-55.5 -207.5t55.5 -207.5 t151 -151t207.5 -55.5t207.5 55.5t151 151t55.5 207.5t-55.5 207.5t-151 151t-207.5 55.5zM454.5 905q22.5 0 38.5 -16t16 -38.5t-16 -39t-38.5 -16.5t-38.5 16.5t-16 39t16 38.5t38.5 16zM754.5 905q22.5 0 38.5 -16t16 -38.5t-16 -39t-38 -16.5q-14 0 -29 10l-55 -145 q17 -23 17 -51q0 -36 -25.5 -61.5t-61.5 -25.5t-61.5 25.5t-25.5 61.5q0 32 20.5 56.5t51.5 29.5l122 126l1 1q-9 14 -9 28q0 23 16 39t38.5 16zM345.5 709q22.5 0 38.5 -16t16 -38.5t-16 -38.5t-38.5 -16t-38.5 16t-16 38.5t16 38.5t38.5 16zM854.5 709q22.5 0 38.5 -16 t16 -38.5t-16 -38.5t-38.5 -16t-38.5 16t-16 38.5t16 38.5t38.5 16z" />
<glyph unicode="&#xe142;" d="M546 173l469 470q91 91 99 192q7 98 -52 175.5t-154 94.5q-22 4 -47 4q-34 0 -66.5 -10t-56.5 -23t-55.5 -38t-48 -41.5t-48.5 -47.5q-376 -375 -391 -390q-30 -27 -45 -41.5t-37.5 -41t-32 -46.5t-16 -47.5t-1.5 -56.5q9 -62 53.5 -95t99.5 -33q74 0 125 51l548 548 q36 36 20 75q-7 16 -21.5 26t-32.5 10q-26 0 -50 -23q-13 -12 -39 -38l-341 -338q-15 -15 -35.5 -15.5t-34.5 13.5t-14 34.5t14 34.5q327 333 361 367q35 35 67.5 51.5t78.5 16.5q14 0 29 -1q44 -8 74.5 -35.5t43.5 -68.5q14 -47 2 -96.5t-47 -84.5q-12 -11 -32 -32 t-79.5 -81t-114.5 -115t-124.5 -123.5t-123 -119.5t-96.5 -89t-57 -45q-56 -27 -120 -27q-70 0 -129 32t-93 89q-48 78 -35 173t81 163l511 511q71 72 111 96q91 55 198 55q80 0 152 -33q78 -36 129.5 -103t66.5 -154q17 -93 -11 -183.5t-94 -156.5l-482 -476 q-15 -15 -36 -16t-37 14t-17.5 34t14.5 35z" />
<glyph unicode="&#xe143;" d="M649 949q48 68 109.5 104t121.5 38.5t118.5 -20t102.5 -64t71 -100.5t27 -123q0 -57 -33.5 -117.5t-94 -124.5t-126.5 -127.5t-150 -152.5t-146 -174q-62 85 -145.5 174t-150 152.5t-126.5 127.5t-93.5 124.5t-33.5 117.5q0 64 28 123t73 100.5t104 64t119 20 t120.5 -38.5t104.5 -104zM896 972q-33 0 -64.5 -19t-56.5 -46t-47.5 -53.5t-43.5 -45.5t-37.5 -19t-36 19t-40 45.5t-43 53.5t-54 46t-65.5 19q-67 0 -122.5 -55.5t-55.5 -132.5q0 -23 13.5 -51t46 -65t57.5 -63t76 -75l22 -22q15 -14 44 -44t50.5 -51t46 -44t41 -35t23 -12 t23.5 12t42.5 36t46 44t52.5 52t44 43q4 4 12 13q43 41 63.5 62t52 55t46 55t26 46t11.5 44q0 79 -53 133.5t-120 54.5z" />
<glyph unicode="&#xe144;" d="M776.5 1214q93.5 0 159.5 -66l141 -141q66 -66 66 -160q0 -42 -28 -95.5t-62 -87.5l-29 -29q-31 53 -77 99l-18 18l95 95l-247 248l-389 -389l212 -212l-105 -106l-19 18l-141 141q-66 66 -66 159t66 159l283 283q65 66 158.5 66zM600 706l105 105q10 -8 19 -17l141 -141 q66 -66 66 -159t-66 -159l-283 -283q-66 -66 -159 -66t-159 66l-141 141q-66 66 -66 159.5t66 159.5l55 55q29 -55 75 -102l18 -17l-95 -95l247 -248l389 389z" />
<glyph unicode="&#xe145;" d="M603 1200q85 0 162 -15t127 -38t79 -48t29 -46v-953q0 -41 -29.5 -70.5t-70.5 -29.5h-600q-41 0 -70.5 29.5t-29.5 70.5v953q0 21 30 46.5t81 48t129 37.5t163 15zM300 1000v-700h600v700h-600zM600 254q-43 0 -73.5 -30.5t-30.5 -73.5t30.5 -73.5t73.5 -30.5t73.5 30.5 t30.5 73.5t-30.5 73.5t-73.5 30.5z" />
<glyph unicode="&#xe146;" d="M902 1185l283 -282q15 -15 15 -36t-14.5 -35.5t-35.5 -14.5t-35 15l-36 35l-279 -267v-300l-212 210l-308 -307l-280 -203l203 280l307 308l-210 212h300l267 279l-35 36q-15 14 -15 35t14.5 35.5t35.5 14.5t35 -15z" />
<glyph unicode="&#xe148;" d="M700 1248v-78q38 -5 72.5 -14.5t75.5 -31.5t71 -53.5t52 -84t24 -118.5h-159q-4 36 -10.5 59t-21 45t-40 35.5t-64.5 20.5v-307l64 -13q34 -7 64 -16.5t70 -32t67.5 -52.5t47.5 -80t20 -112q0 -139 -89 -224t-244 -97v-77h-100v79q-150 16 -237 103q-40 40 -52.5 93.5 t-15.5 139.5h139q5 -77 48.5 -126t117.5 -65v335l-27 8q-46 14 -79 26.5t-72 36t-63 52t-40 72.5t-16 98q0 70 25 126t67.5 92t94.5 57t110 27v77h100zM600 754v274q-29 -4 -50 -11t-42 -21.5t-31.5 -41.5t-10.5 -65q0 -29 7 -50.5t16.5 -34t28.5 -22.5t31.5 -14t37.5 -10 q9 -3 13 -4zM700 547v-310q22 2 42.5 6.5t45 15.5t41.5 27t29 42t12 59.5t-12.5 59.5t-38 44.5t-53 31t-66.5 24.5z" />
<glyph unicode="&#xe149;" d="M561 1197q84 0 160.5 -40t123.5 -109.5t47 -147.5h-153q0 40 -19.5 71.5t-49.5 48.5t-59.5 26t-55.5 9q-37 0 -79 -14.5t-62 -35.5q-41 -44 -41 -101q0 -26 13.5 -63t26.5 -61t37 -66q6 -9 9 -14h241v-100h-197q8 -50 -2.5 -115t-31.5 -95q-45 -62 -99 -112 q34 10 83 17.5t71 7.5q32 1 102 -16t104 -17q83 0 136 30l50 -147q-31 -19 -58 -30.5t-55 -15.5t-42 -4.5t-46 -0.5q-23 0 -76 17t-111 32.5t-96 11.5q-39 -3 -82 -16t-67 -25l-23 -11l-55 145q4 3 16 11t15.5 10.5t13 9t15.5 12t14.5 14t17.5 18.5q48 55 54 126.5 t-30 142.5h-221v100h166q-23 47 -44 104q-7 20 -12 41.5t-6 55.5t6 66.5t29.5 70.5t58.5 71q97 88 263 88z" />
<glyph unicode="&#xe150;" d="M400 300h150q21 0 25 -11t-10 -25l-230 -250q-14 -15 -35 -15t-35 15l-230 250q-14 14 -10 25t25 11h150v900h200v-900zM935 1184l230 -249q14 -14 10 -24.5t-25 -10.5h-150v-900h-200v900h-150q-21 0 -25 10.5t10 24.5l230 249q14 15 35 15t35 -15z" />
<glyph unicode="&#xe151;" d="M1000 700h-100v100h-100v-100h-100v500h300v-500zM400 300h150q21 0 25 -11t-10 -25l-230 -250q-14 -15 -35 -15t-35 15l-230 250q-14 14 -10 25t25 11h150v900h200v-900zM801 1100v-200h100v200h-100zM1000 350l-200 -250h200v-100h-300v150l200 250h-200v100h300v-150z " />
<glyph unicode="&#xe152;" d="M400 300h150q21 0 25 -11t-10 -25l-230 -250q-14 -15 -35 -15t-35 15l-230 250q-14 14 -10 25t25 11h150v900h200v-900zM1000 1050l-200 -250h200v-100h-300v150l200 250h-200v100h300v-150zM1000 0h-100v100h-100v-100h-100v500h300v-500zM801 400v-200h100v200h-100z " />
<glyph unicode="&#xe153;" d="M400 300h150q21 0 25 -11t-10 -25l-230 -250q-14 -15 -35 -15t-35 15l-230 250q-14 14 -10 25t25 11h150v900h200v-900zM1000 700h-100v400h-100v100h200v-500zM1100 0h-100v100h-200v400h300v-500zM901 400v-200h100v200h-100z" />
<glyph unicode="&#xe154;" d="M400 300h150q21 0 25 -11t-10 -25l-230 -250q-14 -15 -35 -15t-35 15l-230 250q-14 14 -10 25t25 11h150v900h200v-900zM1100 700h-100v100h-200v400h300v-500zM901 1100v-200h100v200h-100zM1000 0h-100v400h-100v100h200v-500z" />
<glyph unicode="&#xe155;" d="M400 300h150q21 0 25 -11t-10 -25l-230 -250q-14 -15 -35 -15t-35 15l-230 250q-14 14 -10 25t25 11h150v900h200v-900zM900 1000h-200v200h200v-200zM1000 700h-300v200h300v-200zM1100 400h-400v200h400v-200zM1200 100h-500v200h500v-200z" />
<glyph unicode="&#xe156;" d="M400 300h150q21 0 25 -11t-10 -25l-230 -250q-14 -15 -35 -15t-35 15l-230 250q-14 14 -10 25t25 11h150v900h200v-900zM1200 1000h-500v200h500v-200zM1100 700h-400v200h400v-200zM1000 400h-300v200h300v-200zM900 100h-200v200h200v-200z" />
<glyph unicode="&#xe157;" d="M350 1100h400q162 0 256 -93.5t94 -256.5v-400q0 -165 -93.5 -257.5t-256.5 -92.5h-400q-165 0 -257.5 92.5t-92.5 257.5v400q0 165 92.5 257.5t257.5 92.5zM800 900h-500q-41 0 -70.5 -29.5t-29.5 -70.5v-500q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5 v500q0 41 -29.5 70.5t-70.5 29.5z" />
<glyph unicode="&#xe158;" d="M350 1100h400q165 0 257.5 -92.5t92.5 -257.5v-400q0 -165 -92.5 -257.5t-257.5 -92.5h-400q-163 0 -256.5 92.5t-93.5 257.5v400q0 163 94 256.5t256 93.5zM800 900h-500q-41 0 -70.5 -29.5t-29.5 -70.5v-500q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5 v500q0 41 -29.5 70.5t-70.5 29.5zM440 770l253 -190q17 -12 17 -30t-17 -30l-253 -190q-16 -12 -28 -6.5t-12 26.5v400q0 21 12 26.5t28 -6.5z" />
<glyph unicode="&#xe159;" d="M350 1100h400q163 0 256.5 -94t93.5 -256v-400q0 -165 -92.5 -257.5t-257.5 -92.5h-400q-165 0 -257.5 92.5t-92.5 257.5v400q0 163 92.5 256.5t257.5 93.5zM800 900h-500q-41 0 -70.5 -29.5t-29.5 -70.5v-500q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5 v500q0 41 -29.5 70.5t-70.5 29.5zM350 700h400q21 0 26.5 -12t-6.5 -28l-190 -253q-12 -17 -30 -17t-30 17l-190 253q-12 16 -6.5 28t26.5 12z" />
<glyph unicode="&#xe160;" d="M350 1100h400q165 0 257.5 -92.5t92.5 -257.5v-400q0 -163 -92.5 -256.5t-257.5 -93.5h-400q-163 0 -256.5 94t-93.5 256v400q0 165 92.5 257.5t257.5 92.5zM800 900h-500q-41 0 -70.5 -29.5t-29.5 -70.5v-500q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5 v500q0 41 -29.5 70.5t-70.5 29.5zM580 693l190 -253q12 -16 6.5 -28t-26.5 -12h-400q-21 0 -26.5 12t6.5 28l190 253q12 17 30 17t30 -17z" />
<glyph unicode="&#xe161;" d="M550 1100h400q165 0 257.5 -92.5t92.5 -257.5v-400q0 -165 -92.5 -257.5t-257.5 -92.5h-400q-21 0 -35.5 14.5t-14.5 35.5v100q0 21 14.5 35.5t35.5 14.5h450q41 0 70.5 29.5t29.5 70.5v500q0 41 -29.5 70.5t-70.5 29.5h-450q-21 0 -35.5 14.5t-14.5 35.5v100 q0 21 14.5 35.5t35.5 14.5zM338 867l324 -284q16 -14 16 -33t-16 -33l-324 -284q-16 -14 -27 -9t-11 26v150h-250q-21 0 -35.5 14.5t-14.5 35.5v200q0 21 14.5 35.5t35.5 14.5h250v150q0 21 11 26t27 -9z" />
<glyph unicode="&#xe162;" d="M793 1182l9 -9q8 -10 5 -27q-3 -11 -79 -225.5t-78 -221.5l300 1q24 0 32.5 -17.5t-5.5 -35.5q-1 0 -133.5 -155t-267 -312.5t-138.5 -162.5q-12 -15 -26 -15h-9l-9 8q-9 11 -4 32q2 9 42 123.5t79 224.5l39 110h-302q-23 0 -31 19q-10 21 6 41q75 86 209.5 237.5 t228 257t98.5 111.5q9 16 25 16h9z" />
<glyph unicode="&#xe163;" d="M350 1100h400q21 0 35.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-450q-41 0 -70.5 -29.5t-29.5 -70.5v-500q0 -41 29.5 -70.5t70.5 -29.5h450q21 0 35.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-400q-165 0 -257.5 92.5t-92.5 257.5v400 q0 165 92.5 257.5t257.5 92.5zM938 867l324 -284q16 -14 16 -33t-16 -33l-324 -284q-16 -14 -27 -9t-11 26v150h-250q-21 0 -35.5 14.5t-14.5 35.5v200q0 21 14.5 35.5t35.5 14.5h250v150q0 21 11 26t27 -9z" />
<glyph unicode="&#xe164;" d="M750 1200h400q21 0 35.5 -14.5t14.5 -35.5v-400q0 -21 -10.5 -25t-24.5 10l-109 109l-312 -312q-15 -15 -35.5 -15t-35.5 15l-141 141q-15 15 -15 35.5t15 35.5l312 312l-109 109q-14 14 -10 24.5t25 10.5zM456 900h-156q-41 0 -70.5 -29.5t-29.5 -70.5v-500 q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5v148l200 200v-298q0 -165 -93.5 -257.5t-256.5 -92.5h-400q-165 0 -257.5 92.5t-92.5 257.5v400q0 165 92.5 257.5t257.5 92.5h300z" />
<glyph unicode="&#xe165;" d="M600 1186q119 0 227.5 -46.5t187 -125t125 -187t46.5 -227.5t-46.5 -227.5t-125 -187t-187 -125t-227.5 -46.5t-227.5 46.5t-187 125t-125 187t-46.5 227.5t46.5 227.5t125 187t187 125t227.5 46.5zM600 1022q-115 0 -212 -56.5t-153.5 -153.5t-56.5 -212t56.5 -212 t153.5 -153.5t212 -56.5t212 56.5t153.5 153.5t56.5 212t-56.5 212t-153.5 153.5t-212 56.5zM600 794q80 0 137 -57t57 -137t-57 -137t-137 -57t-137 57t-57 137t57 137t137 57z" />
<glyph unicode="&#xe166;" d="M450 1200h200q21 0 35.5 -14.5t14.5 -35.5v-350h245q20 0 25 -11t-9 -26l-383 -426q-14 -15 -33.5 -15t-32.5 15l-379 426q-13 15 -8.5 26t25.5 11h250v350q0 21 14.5 35.5t35.5 14.5zM50 300h1000q21 0 35.5 -14.5t14.5 -35.5v-250h-1100v250q0 21 14.5 35.5t35.5 14.5z M900 200v-50h100v50h-100z" />
<glyph unicode="&#xe167;" d="M583 1182l378 -435q14 -15 9 -31t-26 -16h-244v-250q0 -20 -17 -35t-39 -15h-200q-20 0 -32 14.5t-12 35.5v250h-250q-20 0 -25.5 16.5t8.5 31.5l383 431q14 16 33.5 17t33.5 -14zM50 300h1000q21 0 35.5 -14.5t14.5 -35.5v-250h-1100v250q0 21 14.5 35.5t35.5 14.5z M900 200v-50h100v50h-100z" />
<glyph unicode="&#xe168;" d="M396 723l369 369q7 7 17.5 7t17.5 -7l139 -139q7 -8 7 -18.5t-7 -17.5l-525 -525q-7 -8 -17.5 -8t-17.5 8l-292 291q-7 8 -7 18t7 18l139 139q8 7 18.5 7t17.5 -7zM50 300h1000q21 0 35.5 -14.5t14.5 -35.5v-250h-1100v250q0 21 14.5 35.5t35.5 14.5zM900 200v-50h100v50 h-100z" />
<glyph unicode="&#xe169;" d="M135 1023l142 142q14 14 35 14t35 -14l77 -77l-212 -212l-77 76q-14 15 -14 36t14 35zM655 855l210 210q14 14 24.5 10t10.5 -25l-2 -599q-1 -20 -15.5 -35t-35.5 -15l-597 -1q-21 0 -25 10.5t10 24.5l208 208l-154 155l212 212zM50 300h1000q21 0 35.5 -14.5t14.5 -35.5 v-250h-1100v250q0 21 14.5 35.5t35.5 14.5zM900 200v-50h100v50h-100z" />
<glyph unicode="&#xe170;" d="M350 1200l599 -2q20 -1 35 -15.5t15 -35.5l1 -597q0 -21 -10.5 -25t-24.5 10l-208 208l-155 -154l-212 212l155 154l-210 210q-14 14 -10 24.5t25 10.5zM524 512l-76 -77q-15 -14 -36 -14t-35 14l-142 142q-14 14 -14 35t14 35l77 77zM50 300h1000q21 0 35.5 -14.5 t14.5 -35.5v-250h-1100v250q0 21 14.5 35.5t35.5 14.5zM900 200v-50h100v50h-100z" />
<glyph unicode="&#xe171;" d="M1200 103l-483 276l-314 -399v423h-399l1196 796v-1096zM483 424v-230l683 953z" />
<glyph unicode="&#xe172;" d="M1100 1000v-850q0 -21 -14.5 -35.5t-35.5 -14.5h-150v400h-700v-400h-150q-21 0 -35.5 14.5t-14.5 35.5v1000q0 20 14.5 35t35.5 15h250v-300h500v300h100zM700 1000h-100v200h100v-200z" />
<glyph unicode="&#xe173;" d="M1100 1000l-2 -149l-299 -299l-95 95q-9 9 -21.5 9t-21.5 -9l-149 -147h-312v-400h-150q-21 0 -35.5 14.5t-14.5 35.5v1000q0 20 14.5 35t35.5 15h250v-300h500v300h100zM700 1000h-100v200h100v-200zM1132 638l106 -106q7 -7 7 -17.5t-7 -17.5l-420 -421q-8 -7 -18 -7 t-18 7l-202 203q-8 7 -8 17.5t8 17.5l106 106q7 8 17.5 8t17.5 -8l79 -79l297 297q7 7 17.5 7t17.5 -7z" />
<glyph unicode="&#xe174;" d="M1100 1000v-269l-103 -103l-134 134q-15 15 -33.5 16.5t-34.5 -12.5l-266 -266h-329v-400h-150q-21 0 -35.5 14.5t-14.5 35.5v1000q0 20 14.5 35t35.5 15h250v-300h500v300h100zM700 1000h-100v200h100v-200zM1202 572l70 -70q15 -15 15 -35.5t-15 -35.5l-131 -131 l131 -131q15 -15 15 -35.5t-15 -35.5l-70 -70q-15 -15 -35.5 -15t-35.5 15l-131 131l-131 -131q-15 -15 -35.5 -15t-35.5 15l-70 70q-15 15 -15 35.5t15 35.5l131 131l-131 131q-15 15 -15 35.5t15 35.5l70 70q15 15 35.5 15t35.5 -15l131 -131l131 131q15 15 35.5 15 t35.5 -15z" />
<glyph unicode="&#xe175;" d="M1100 1000v-300h-350q-21 0 -35.5 -14.5t-14.5 -35.5v-150h-500v-400h-150q-21 0 -35.5 14.5t-14.5 35.5v1000q0 20 14.5 35t35.5 15h250v-300h500v300h100zM700 1000h-100v200h100v-200zM850 600h100q21 0 35.5 -14.5t14.5 -35.5v-250h150q21 0 25 -10.5t-10 -24.5 l-230 -230q-14 -14 -35 -14t-35 14l-230 230q-14 14 -10 24.5t25 10.5h150v250q0 21 14.5 35.5t35.5 14.5z" />
<glyph unicode="&#xe176;" d="M1100 1000v-400l-165 165q-14 15 -35 15t-35 -15l-263 -265h-402v-400h-150q-21 0 -35.5 14.5t-14.5 35.5v1000q0 20 14.5 35t35.5 15h250v-300h500v300h100zM700 1000h-100v200h100v-200zM935 565l230 -229q14 -15 10 -25.5t-25 -10.5h-150v-250q0 -20 -14.5 -35 t-35.5 -15h-100q-21 0 -35.5 15t-14.5 35v250h-150q-21 0 -25 10.5t10 25.5l230 229q14 15 35 15t35 -15z" />
<glyph unicode="&#xe177;" d="M50 1100h1100q21 0 35.5 -14.5t14.5 -35.5v-150h-1200v150q0 21 14.5 35.5t35.5 14.5zM1200 800v-550q0 -21 -14.5 -35.5t-35.5 -14.5h-1100q-21 0 -35.5 14.5t-14.5 35.5v550h1200zM100 500v-200h400v200h-400z" />
<glyph unicode="&#xe178;" d="M935 1165l248 -230q14 -14 14 -35t-14 -35l-248 -230q-14 -14 -24.5 -10t-10.5 25v150h-400v200h400v150q0 21 10.5 25t24.5 -10zM200 800h-50q-21 0 -35.5 14.5t-14.5 35.5v100q0 21 14.5 35.5t35.5 14.5h50v-200zM400 800h-100v200h100v-200zM18 435l247 230 q14 14 24.5 10t10.5 -25v-150h400v-200h-400v-150q0 -21 -10.5 -25t-24.5 10l-247 230q-15 14 -15 35t15 35zM900 300h-100v200h100v-200zM1000 500h51q20 0 34.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-34.5 -14.5h-51v200z" />
<glyph unicode="&#xe179;" d="M862 1073l276 116q25 18 43.5 8t18.5 -41v-1106q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5v397q-4 1 -11 5t-24 17.5t-30 29t-24 42t-11 56.5v359q0 31 18.5 65t43.5 52zM550 1200q22 0 34.5 -12.5t14.5 -24.5l1 -13v-450q0 -28 -10.5 -59.5 t-25 -56t-29 -45t-25.5 -31.5l-10 -11v-447q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5v447q-4 4 -11 11.5t-24 30.5t-30 46t-24 55t-11 60v450q0 2 0.5 5.5t4 12t8.5 15t14.5 12t22.5 5.5q20 0 32.5 -12.5t14.5 -24.5l3 -13v-350h100v350v5.5t2.5 12 t7 15t15 12t25.5 5.5q23 0 35.5 -12.5t13.5 -24.5l1 -13v-350h100v350q0 2 0.5 5.5t3 12t7 15t15 12t24.5 5.5z" />
<glyph unicode="&#xe180;" d="M1200 1100v-56q-4 0 -11 -0.5t-24 -3t-30 -7.5t-24 -15t-11 -24v-888q0 -22 25 -34.5t50 -13.5l25 -2v-56h-400v56q75 0 87.5 6.5t12.5 43.5v394h-500v-394q0 -37 12.5 -43.5t87.5 -6.5v-56h-400v56q4 0 11 0.5t24 3t30 7.5t24 15t11 24v888q0 22 -25 34.5t-50 13.5 l-25 2v56h400v-56q-75 0 -87.5 -6.5t-12.5 -43.5v-394h500v394q0 37 -12.5 43.5t-87.5 6.5v56h400z" />
<glyph unicode="&#xe181;" d="M675 1000h375q21 0 35.5 -14.5t14.5 -35.5v-150h-105l-295 -98v98l-200 200h-400l100 100h375zM100 900h300q41 0 70.5 -29.5t29.5 -70.5v-500q0 -41 -29.5 -70.5t-70.5 -29.5h-300q-41 0 -70.5 29.5t-29.5 70.5v500q0 41 29.5 70.5t70.5 29.5zM100 800v-200h300v200 h-300zM1100 535l-400 -133v163l400 133v-163zM100 500v-200h300v200h-300zM1100 398v-248q0 -21 -14.5 -35.5t-35.5 -14.5h-375l-100 -100h-375l-100 100h400l200 200h105z" />
<glyph unicode="&#xe182;" d="M17 1007l162 162q17 17 40 14t37 -22l139 -194q14 -20 11 -44.5t-20 -41.5l-119 -118q102 -142 228 -268t267 -227l119 118q17 17 42.5 19t44.5 -12l192 -136q19 -14 22.5 -37.5t-13.5 -40.5l-163 -162q-3 -1 -9.5 -1t-29.5 2t-47.5 6t-62.5 14.5t-77.5 26.5t-90 42.5 t-101.5 60t-111 83t-119 108.5q-74 74 -133.5 150.5t-94.5 138.5t-60 119.5t-34.5 100t-15 74.5t-4.5 48z" />
<glyph unicode="&#xe183;" d="M600 1100q92 0 175 -10.5t141.5 -27t108.5 -36.5t81.5 -40t53.5 -37t31 -27l9 -10v-200q0 -21 -14.5 -33t-34.5 -9l-202 34q-20 3 -34.5 20t-14.5 38v146q-141 24 -300 24t-300 -24v-146q0 -21 -14.5 -38t-34.5 -20l-202 -34q-20 -3 -34.5 9t-14.5 33v200q3 4 9.5 10.5 t31 26t54 37.5t80.5 39.5t109 37.5t141 26.5t175 10.5zM600 795q56 0 97 -9.5t60 -23.5t30 -28t12 -24l1 -10v-50l365 -303q14 -15 24.5 -40t10.5 -45v-212q0 -21 -14.5 -35.5t-35.5 -14.5h-1100q-21 0 -35.5 14.5t-14.5 35.5v212q0 20 10.5 45t24.5 40l365 303v50 q0 4 1 10.5t12 23t30 29t60 22.5t97 10z" />
<glyph unicode="&#xe184;" d="M1100 700l-200 -200h-600l-200 200v500h200v-200h200v200h200v-200h200v200h200v-500zM250 400h700q21 0 35.5 -14.5t14.5 -35.5t-14.5 -35.5t-35.5 -14.5h-12l137 -100h-950l137 100h-12q-21 0 -35.5 14.5t-14.5 35.5t14.5 35.5t35.5 14.5zM50 100h1100q21 0 35.5 -14.5 t14.5 -35.5v-50h-1200v50q0 21 14.5 35.5t35.5 14.5z" />
<glyph unicode="&#xe185;" d="M700 1100h-100q-41 0 -70.5 -29.5t-29.5 -70.5v-1000h300v1000q0 41 -29.5 70.5t-70.5 29.5zM1100 800h-100q-41 0 -70.5 -29.5t-29.5 -70.5v-700h300v700q0 41 -29.5 70.5t-70.5 29.5zM400 0h-300v400q0 41 29.5 70.5t70.5 29.5h100q41 0 70.5 -29.5t29.5 -70.5v-400z " />
<glyph unicode="&#xe186;" d="M200 1100h700q124 0 212 -88t88 -212v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212v500q0 124 88 212t212 88zM100 900v-700h900v700h-900zM500 700h-200v-100h200v-300h-300v100h200v100h-200v300h300v-100zM900 700v-300l-100 -100h-200v500h200z M700 700v-300h100v300h-100z" />
<glyph unicode="&#xe187;" d="M200 1100h700q124 0 212 -88t88 -212v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212v500q0 124 88 212t212 88zM100 900v-700h900v700h-900zM500 300h-100v200h-100v-200h-100v500h100v-200h100v200h100v-500zM900 700v-300l-100 -100h-200v500h200z M700 700v-300h100v300h-100z" />
<glyph unicode="&#xe188;" d="M200 1100h700q124 0 212 -88t88 -212v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212v500q0 124 88 212t212 88zM100 900v-700h900v700h-900zM500 700h-200v-300h200v-100h-300v500h300v-100zM900 700h-200v-300h200v-100h-300v500h300v-100z" />
<glyph unicode="&#xe189;" d="M200 1100h700q124 0 212 -88t88 -212v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212v500q0 124 88 212t212 88zM100 900v-700h900v700h-900zM500 400l-300 150l300 150v-300zM900 550l-300 -150v300z" />
<glyph unicode="&#xe190;" d="M200 1100h700q124 0 212 -88t88 -212v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212v500q0 124 88 212t212 88zM100 900v-700h900v700h-900zM900 300h-700v500h700v-500zM800 700h-130q-38 0 -66.5 -43t-28.5 -108t27 -107t68 -42h130v300zM300 700v-300 h130q41 0 68 42t27 107t-28.5 108t-66.5 43h-130z" />
<glyph unicode="&#xe191;" d="M200 1100h700q124 0 212 -88t88 -212v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212v500q0 124 88 212t212 88zM100 900v-700h900v700h-900zM500 700h-200v-100h200v-300h-300v100h200v100h-200v300h300v-100zM900 300h-100v400h-100v100h200v-500z M700 300h-100v100h100v-100z" />
<glyph unicode="&#xe192;" d="M200 1100h700q124 0 212 -88t88 -212v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212v500q0 124 88 212t212 88zM100 900v-700h900v700h-900zM300 700h200v-400h-300v500h100v-100zM900 300h-100v400h-100v100h200v-500zM300 600v-200h100v200h-100z M700 300h-100v100h100v-100z" />
<glyph unicode="&#xe193;" d="M200 1100h700q124 0 212 -88t88 -212v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212v500q0 124 88 212t212 88zM100 900v-700h900v700h-900zM500 500l-199 -200h-100v50l199 200v150h-200v100h300v-300zM900 300h-100v400h-100v100h200v-500zM701 300h-100 v100h100v-100z" />
<glyph unicode="&#xe194;" d="M600 1191q120 0 229.5 -47t188.5 -126t126 -188.5t47 -229.5t-47 -229.5t-126 -188.5t-188.5 -126t-229.5 -47t-229.5 47t-188.5 126t-126 188.5t-47 229.5t47 229.5t126 188.5t188.5 126t229.5 47zM600 1021q-114 0 -211 -56.5t-153.5 -153.5t-56.5 -211t56.5 -211 t153.5 -153.5t211 -56.5t211 56.5t153.5 153.5t56.5 211t-56.5 211t-153.5 153.5t-211 56.5zM800 700h-300v-200h300v-100h-300l-100 100v200l100 100h300v-100z" />
<glyph unicode="&#xe195;" d="M600 1191q120 0 229.5 -47t188.5 -126t126 -188.5t47 -229.5t-47 -229.5t-126 -188.5t-188.5 -126t-229.5 -47t-229.5 47t-188.5 126t-126 188.5t-47 229.5t47 229.5t126 188.5t188.5 126t229.5 47zM600 1021q-114 0 -211 -56.5t-153.5 -153.5t-56.5 -211t56.5 -211 t153.5 -153.5t211 -56.5t211 56.5t153.5 153.5t56.5 211t-56.5 211t-153.5 153.5t-211 56.5zM800 700v-100l-50 -50l100 -100v-50h-100l-100 100h-150v-100h-100v400h300zM500 700v-100h200v100h-200z" />
<glyph unicode="&#xe197;" d="M503 1089q110 0 200.5 -59.5t134.5 -156.5q44 14 90 14q120 0 205 -86.5t85 -207t-85 -207t-205 -86.5h-128v250q0 21 -14.5 35.5t-35.5 14.5h-300q-21 0 -35.5 -14.5t-14.5 -35.5v-250h-222q-80 0 -136 57.5t-56 136.5q0 69 43 122.5t108 67.5q-2 19 -2 37q0 100 49 185 t134 134t185 49zM525 500h150q10 0 17.5 -7.5t7.5 -17.5v-275h137q21 0 26 -11.5t-8 -27.5l-223 -244q-13 -16 -32 -16t-32 16l-223 244q-13 16 -8 27.5t26 11.5h137v275q0 10 7.5 17.5t17.5 7.5z" />
<glyph unicode="&#xe198;" d="M502 1089q110 0 201 -59.5t135 -156.5q43 15 89 15q121 0 206 -86.5t86 -206.5q0 -99 -60 -181t-150 -110l-378 360q-13 16 -31.5 16t-31.5 -16l-381 -365h-9q-79 0 -135.5 57.5t-56.5 136.5q0 69 43 122.5t108 67.5q-2 19 -2 38q0 100 49 184.5t133.5 134t184.5 49.5z M632 467l223 -228q13 -16 8 -27.5t-26 -11.5h-137v-275q0 -10 -7.5 -17.5t-17.5 -7.5h-150q-10 0 -17.5 7.5t-7.5 17.5v275h-137q-21 0 -26 11.5t8 27.5q199 204 223 228q19 19 31.5 19t32.5 -19z" />
<glyph unicode="&#xe199;" d="M700 100v100h400l-270 300h170l-270 300h170l-300 333l-300 -333h170l-270 -300h170l-270 -300h400v-100h-50q-21 0 -35.5 -14.5t-14.5 -35.5v-50h400v50q0 21 -14.5 35.5t-35.5 14.5h-50z" />
<glyph unicode="&#xe200;" d="M600 1179q94 0 167.5 -56.5t99.5 -145.5q89 -6 150.5 -71.5t61.5 -155.5q0 -61 -29.5 -112.5t-79.5 -82.5q9 -29 9 -55q0 -74 -52.5 -126.5t-126.5 -52.5q-55 0 -100 30v-251q21 0 35.5 -14.5t14.5 -35.5v-50h-300v50q0 21 14.5 35.5t35.5 14.5v251q-45 -30 -100 -30 q-74 0 -126.5 52.5t-52.5 126.5q0 18 4 38q-47 21 -75.5 65t-28.5 97q0 74 52.5 126.5t126.5 52.5q5 0 23 -2q0 2 -1 10t-1 13q0 116 81.5 197.5t197.5 81.5z" />
<glyph unicode="&#xe201;" d="M1010 1010q111 -111 150.5 -260.5t0 -299t-150.5 -260.5q-83 -83 -191.5 -126.5t-218.5 -43.5t-218.5 43.5t-191.5 126.5q-111 111 -150.5 260.5t0 299t150.5 260.5q83 83 191.5 126.5t218.5 43.5t218.5 -43.5t191.5 -126.5zM476 1065q-4 0 -8 -1q-121 -34 -209.5 -122.5 t-122.5 -209.5q-4 -12 2.5 -23t18.5 -14l36 -9q3 -1 7 -1q23 0 29 22q27 96 98 166q70 71 166 98q11 3 17.5 13.5t3.5 22.5l-9 35q-3 13 -14 19q-7 4 -15 4zM512 920q-4 0 -9 -2q-80 -24 -138.5 -82.5t-82.5 -138.5q-4 -13 2 -24t19 -14l34 -9q4 -1 8 -1q22 0 28 21 q18 58 58.5 98.5t97.5 58.5q12 3 18 13.5t3 21.5l-9 35q-3 12 -14 19q-7 4 -15 4zM719.5 719.5q-49.5 49.5 -119.5 49.5t-119.5 -49.5t-49.5 -119.5t49.5 -119.5t119.5 -49.5t119.5 49.5t49.5 119.5t-49.5 119.5zM855 551q-22 0 -28 -21q-18 -58 -58.5 -98.5t-98.5 -57.5 q-11 -4 -17 -14.5t-3 -21.5l9 -35q3 -12 14 -19q7 -4 15 -4q4 0 9 2q80 24 138.5 82.5t82.5 138.5q4 13 -2.5 24t-18.5 14l-34 9q-4 1 -8 1zM1000 515q-23 0 -29 -22q-27 -96 -98 -166q-70 -71 -166 -98q-11 -3 -17.5 -13.5t-3.5 -22.5l9 -35q3 -13 14 -19q7 -4 15 -4 q4 0 8 1q121 34 209.5 122.5t122.5 209.5q4 12 -2.5 23t-18.5 14l-36 9q-3 1 -7 1z" />
<glyph unicode="&#xe202;" d="M700 800h300v-380h-180v200h-340v-200h-380v755q0 10 7.5 17.5t17.5 7.5h575v-400zM1000 900h-200v200zM700 300h162l-212 -212l-212 212h162v200h100v-200zM520 0h-395q-10 0 -17.5 7.5t-7.5 17.5v395zM1000 220v-195q0 -10 -7.5 -17.5t-17.5 -7.5h-195z" />
<glyph unicode="&#xe203;" d="M700 800h300v-520l-350 350l-550 -550v1095q0 10 7.5 17.5t17.5 7.5h575v-400zM1000 900h-200v200zM862 200h-162v-200h-100v200h-162l212 212zM480 0h-355q-10 0 -17.5 7.5t-7.5 17.5v55h380v-80zM1000 80v-55q0 -10 -7.5 -17.5t-17.5 -7.5h-155v80h180z" />
<glyph unicode="&#xe204;" d="M1162 800h-162v-200h100l100 -100h-300v300h-162l212 212zM200 800h200q27 0 40 -2t29.5 -10.5t23.5 -30t7 -57.5h300v-100h-600l-200 -350v450h100q0 36 7 57.5t23.5 30t29.5 10.5t40 2zM800 400h240l-240 -400h-800l300 500h500v-100z" />
<glyph unicode="&#xe205;" d="M650 1100h100q21 0 35.5 -14.5t14.5 -35.5v-50h50q21 0 35.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-300q-21 0 -35.5 14.5t-14.5 35.5v100q0 21 14.5 35.5t35.5 14.5h50v50q0 21 14.5 35.5t35.5 14.5zM1000 850v150q41 0 70.5 -29.5t29.5 -70.5v-800 q0 -41 -29.5 -70.5t-70.5 -29.5h-600q-1 0 -20 4l246 246l-326 326v324q0 41 29.5 70.5t70.5 29.5v-150q0 -62 44 -106t106 -44h300q62 0 106 44t44 106zM412 250l-212 -212v162h-200v100h200v162z" />
<glyph unicode="&#xe206;" d="M450 1100h100q21 0 35.5 -14.5t14.5 -35.5v-50h50q21 0 35.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-300q-21 0 -35.5 14.5t-14.5 35.5v100q0 21 14.5 35.5t35.5 14.5h50v50q0 21 14.5 35.5t35.5 14.5zM800 850v150q41 0 70.5 -29.5t29.5 -70.5v-500 h-200v-300h200q0 -36 -7 -57.5t-23.5 -30t-29.5 -10.5t-40 -2h-600q-41 0 -70.5 29.5t-29.5 70.5v800q0 41 29.5 70.5t70.5 29.5v-150q0 -62 44 -106t106 -44h300q62 0 106 44t44 106zM1212 250l-212 -212v162h-200v100h200v162z" />
<glyph unicode="&#xe209;" d="M658 1197l637 -1104q23 -38 7 -65.5t-60 -27.5h-1276q-44 0 -60 27.5t7 65.5l637 1104q22 39 54 39t54 -39zM704 800h-208q-20 0 -32 -14.5t-8 -34.5l58 -302q4 -20 21.5 -34.5t37.5 -14.5h54q20 0 37.5 14.5t21.5 34.5l58 302q4 20 -8 34.5t-32 14.5zM500 300v-100h200 v100h-200z" />
<glyph unicode="&#xe210;" d="M425 1100h250q10 0 17.5 -7.5t7.5 -17.5v-150q0 -10 -7.5 -17.5t-17.5 -7.5h-250q-10 0 -17.5 7.5t-7.5 17.5v150q0 10 7.5 17.5t17.5 7.5zM425 800h250q10 0 17.5 -7.5t7.5 -17.5v-150q0 -10 -7.5 -17.5t-17.5 -7.5h-250q-10 0 -17.5 7.5t-7.5 17.5v150q0 10 7.5 17.5 t17.5 7.5zM825 800h250q10 0 17.5 -7.5t7.5 -17.5v-150q0 -10 -7.5 -17.5t-17.5 -7.5h-250q-10 0 -17.5 7.5t-7.5 17.5v150q0 10 7.5 17.5t17.5 7.5zM25 500h250q10 0 17.5 -7.5t7.5 -17.5v-150q0 -10 -7.5 -17.5t-17.5 -7.5h-250q-10 0 -17.5 7.5t-7.5 17.5v150 q0 10 7.5 17.5t17.5 7.5zM425 500h250q10 0 17.5 -7.5t7.5 -17.5v-150q0 -10 -7.5 -17.5t-17.5 -7.5h-250q-10 0 -17.5 7.5t-7.5 17.5v150q0 10 7.5 17.5t17.5 7.5zM825 500h250q10 0 17.5 -7.5t7.5 -17.5v-150q0 -10 -7.5 -17.5t-17.5 -7.5h-250q-10 0 -17.5 7.5t-7.5 17.5 v150q0 10 7.5 17.5t17.5 7.5zM25 200h250q10 0 17.5 -7.5t7.5 -17.5v-150q0 -10 -7.5 -17.5t-17.5 -7.5h-250q-10 0 -17.5 7.5t-7.5 17.5v150q0 10 7.5 17.5t17.5 7.5zM425 200h250q10 0 17.5 -7.5t7.5 -17.5v-150q0 -10 -7.5 -17.5t-17.5 -7.5h-250q-10 0 -17.5 7.5 t-7.5 17.5v150q0 10 7.5 17.5t17.5 7.5zM825 200h250q10 0 17.5 -7.5t7.5 -17.5v-150q0 -10 -7.5 -17.5t-17.5 -7.5h-250q-10 0 -17.5 7.5t-7.5 17.5v150q0 10 7.5 17.5t17.5 7.5z" />
<glyph unicode="&#xe211;" d="M700 1200h100v-200h-100v-100h350q62 0 86.5 -39.5t-3.5 -94.5l-66 -132q-41 -83 -81 -134h-772q-40 51 -81 134l-66 132q-28 55 -3.5 94.5t86.5 39.5h350v100h-100v200h100v100h200v-100zM250 400h700q21 0 35.5 -14.5t14.5 -35.5t-14.5 -35.5t-35.5 -14.5h-12l137 -100 h-950l138 100h-13q-21 0 -35.5 14.5t-14.5 35.5t14.5 35.5t35.5 14.5zM50 100h1100q21 0 35.5 -14.5t14.5 -35.5v-50h-1200v50q0 21 14.5 35.5t35.5 14.5z" />
<glyph unicode="&#xe212;" d="M600 1300q40 0 68.5 -29.5t28.5 -70.5h-194q0 41 28.5 70.5t68.5 29.5zM443 1100h314q18 -37 18 -75q0 -8 -3 -25h328q41 0 44.5 -16.5t-30.5 -38.5l-175 -145h-678l-178 145q-34 22 -29 38.5t46 16.5h328q-3 17 -3 25q0 38 18 75zM250 700h700q21 0 35.5 -14.5 t14.5 -35.5t-14.5 -35.5t-35.5 -14.5h-150v-200l275 -200h-950l275 200v200h-150q-21 0 -35.5 14.5t-14.5 35.5t14.5 35.5t35.5 14.5zM50 100h1100q21 0 35.5 -14.5t14.5 -35.5v-50h-1200v50q0 21 14.5 35.5t35.5 14.5z" />
<glyph unicode="&#xe213;" d="M600 1181q75 0 128 -53t53 -128t-53 -128t-128 -53t-128 53t-53 128t53 128t128 53zM602 798h46q34 0 55.5 -28.5t21.5 -86.5q0 -76 39 -183h-324q39 107 39 183q0 58 21.5 86.5t56.5 28.5h45zM250 400h700q21 0 35.5 -14.5t14.5 -35.5t-14.5 -35.5t-35.5 -14.5h-13 l138 -100h-950l137 100h-12q-21 0 -35.5 14.5t-14.5 35.5t14.5 35.5t35.5 14.5zM50 100h1100q21 0 35.5 -14.5t14.5 -35.5v-50h-1200v50q0 21 14.5 35.5t35.5 14.5z" />
<glyph unicode="&#xe214;" d="M600 1300q47 0 92.5 -53.5t71 -123t25.5 -123.5q0 -78 -55.5 -133.5t-133.5 -55.5t-133.5 55.5t-55.5 133.5q0 62 34 143l144 -143l111 111l-163 163q34 26 63 26zM602 798h46q34 0 55.5 -28.5t21.5 -86.5q0 -76 39 -183h-324q39 107 39 183q0 58 21.5 86.5t56.5 28.5h45 zM250 400h700q21 0 35.5 -14.5t14.5 -35.5t-14.5 -35.5t-35.5 -14.5h-13l138 -100h-950l137 100h-12q-21 0 -35.5 14.5t-14.5 35.5t14.5 35.5t35.5 14.5zM50 100h1100q21 0 35.5 -14.5t14.5 -35.5v-50h-1200v50q0 21 14.5 35.5t35.5 14.5z" />
<glyph unicode="&#xe215;" d="M600 1200l300 -161v-139h-300q0 -57 18.5 -108t50 -91.5t63 -72t70 -67.5t57.5 -61h-530q-60 83 -90.5 177.5t-30.5 178.5t33 164.5t87.5 139.5t126 96.5t145.5 41.5v-98zM250 400h700q21 0 35.5 -14.5t14.5 -35.5t-14.5 -35.5t-35.5 -14.5h-13l138 -100h-950l137 100 h-12q-21 0 -35.5 14.5t-14.5 35.5t14.5 35.5t35.5 14.5zM50 100h1100q21 0 35.5 -14.5t14.5 -35.5v-50h-1200v50q0 21 14.5 35.5t35.5 14.5z" />
<glyph unicode="&#xe216;" d="M600 1300q41 0 70.5 -29.5t29.5 -70.5v-78q46 -26 73 -72t27 -100v-50h-400v50q0 54 27 100t73 72v78q0 41 29.5 70.5t70.5 29.5zM400 800h400q54 0 100 -27t72 -73h-172v-100h200v-100h-200v-100h200v-100h-200v-100h200q0 -83 -58.5 -141.5t-141.5 -58.5h-400 q-83 0 -141.5 58.5t-58.5 141.5v400q0 83 58.5 141.5t141.5 58.5z" />
<glyph unicode="&#xe218;" d="M150 1100h900q21 0 35.5 -14.5t14.5 -35.5v-500q0 -21 -14.5 -35.5t-35.5 -14.5h-900q-21 0 -35.5 14.5t-14.5 35.5v500q0 21 14.5 35.5t35.5 14.5zM125 400h950q10 0 17.5 -7.5t7.5 -17.5v-50q0 -10 -7.5 -17.5t-17.5 -7.5h-283l224 -224q13 -13 13 -31.5t-13 -32 t-31.5 -13.5t-31.5 13l-88 88h-524l-87 -88q-13 -13 -32 -13t-32 13.5t-13 32t13 31.5l224 224h-289q-10 0 -17.5 7.5t-7.5 17.5v50q0 10 7.5 17.5t17.5 7.5zM541 300l-100 -100h324l-100 100h-124z" />
<glyph unicode="&#xe219;" d="M200 1100h800q83 0 141.5 -58.5t58.5 -141.5v-200h-100q0 41 -29.5 70.5t-70.5 29.5h-250q-41 0 -70.5 -29.5t-29.5 -70.5h-100q0 41 -29.5 70.5t-70.5 29.5h-250q-41 0 -70.5 -29.5t-29.5 -70.5h-100v200q0 83 58.5 141.5t141.5 58.5zM100 600h1000q41 0 70.5 -29.5 t29.5 -70.5v-300h-1200v300q0 41 29.5 70.5t70.5 29.5zM300 100v-50q0 -21 -14.5 -35.5t-35.5 -14.5h-100q-21 0 -35.5 14.5t-14.5 35.5v50h200zM1100 100v-50q0 -21 -14.5 -35.5t-35.5 -14.5h-100q-21 0 -35.5 14.5t-14.5 35.5v50h200z" />
<glyph unicode="&#xe221;" d="M480 1165l682 -683q31 -31 31 -75.5t-31 -75.5l-131 -131h-481l-517 518q-32 31 -32 75.5t32 75.5l295 296q31 31 75.5 31t76.5 -31zM108 794l342 -342l303 304l-341 341zM250 100h800q21 0 35.5 -14.5t14.5 -35.5v-50h-900v50q0 21 14.5 35.5t35.5 14.5z" />
<glyph unicode="&#xe223;" d="M1057 647l-189 506q-8 19 -27.5 33t-40.5 14h-400q-21 0 -40.5 -14t-27.5 -33l-189 -506q-8 -19 1.5 -33t30.5 -14h625v-150q0 -21 14.5 -35.5t35.5 -14.5t35.5 14.5t14.5 35.5v150h125q21 0 30.5 14t1.5 33zM897 0h-595v50q0 21 14.5 35.5t35.5 14.5h50v50 q0 21 14.5 35.5t35.5 14.5h48v300h200v-300h47q21 0 35.5 -14.5t14.5 -35.5v-50h50q21 0 35.5 -14.5t14.5 -35.5v-50z" />
<glyph unicode="&#xe224;" d="M900 800h300v-575q0 -10 -7.5 -17.5t-17.5 -7.5h-375v591l-300 300v84q0 10 7.5 17.5t17.5 7.5h375v-400zM1200 900h-200v200zM400 600h300v-575q0 -10 -7.5 -17.5t-17.5 -7.5h-650q-10 0 -17.5 7.5t-7.5 17.5v950q0 10 7.5 17.5t17.5 7.5h375v-400zM700 700h-200v200z " />
<glyph unicode="&#xe225;" d="M484 1095h195q75 0 146 -32.5t124 -86t89.5 -122.5t48.5 -142q18 -14 35 -20q31 -10 64.5 6.5t43.5 48.5q10 34 -15 71q-19 27 -9 43q5 8 12.5 11t19 -1t23.5 -16q41 -44 39 -105q-3 -63 -46 -106.5t-104 -43.5h-62q-7 -55 -35 -117t-56 -100l-39 -234q-3 -20 -20 -34.5 t-38 -14.5h-100q-21 0 -33 14.5t-9 34.5l12 70q-49 -14 -91 -14h-195q-24 0 -65 8l-11 -64q-3 -20 -20 -34.5t-38 -14.5h-100q-21 0 -33 14.5t-9 34.5l26 157q-84 74 -128 175l-159 53q-19 7 -33 26t-14 40v50q0 21 14.5 35.5t35.5 14.5h124q11 87 56 166l-111 95 q-16 14 -12.5 23.5t24.5 9.5h203q116 101 250 101zM675 1000h-250q-10 0 -17.5 -7.5t-7.5 -17.5v-50q0 -10 7.5 -17.5t17.5 -7.5h250q10 0 17.5 7.5t7.5 17.5v50q0 10 -7.5 17.5t-17.5 7.5z" />
<glyph unicode="&#xe226;" d="M641 900l423 247q19 8 42 2.5t37 -21.5l32 -38q14 -15 12.5 -36t-17.5 -34l-139 -120h-390zM50 1100h106q67 0 103 -17t66 -71l102 -212h823q21 0 35.5 -14.5t14.5 -35.5v-50q0 -21 -14 -40t-33 -26l-737 -132q-23 -4 -40 6t-26 25q-42 67 -100 67h-300q-62 0 -106 44 t-44 106v200q0 62 44 106t106 44zM173 928h-80q-19 0 -28 -14t-9 -35v-56q0 -51 42 -51h134q16 0 21.5 8t5.5 24q0 11 -16 45t-27 51q-18 28 -43 28zM550 727q-32 0 -54.5 -22.5t-22.5 -54.5t22.5 -54.5t54.5 -22.5t54.5 22.5t22.5 54.5t-22.5 54.5t-54.5 22.5zM130 389 l152 130q18 19 34 24t31 -3.5t24.5 -17.5t25.5 -28q28 -35 50.5 -51t48.5 -13l63 5l48 -179q13 -61 -3.5 -97.5t-67.5 -79.5l-80 -69q-47 -40 -109 -35.5t-103 51.5l-130 151q-40 47 -35.5 109.5t51.5 102.5zM380 377l-102 -88q-31 -27 2 -65l37 -43q13 -15 27.5 -19.5 t31.5 6.5l61 53q19 16 14 49q-2 20 -12 56t-17 45q-11 12 -19 14t-23 -8z" />
<glyph unicode="&#xe227;" d="M625 1200h150q10 0 17.5 -7.5t7.5 -17.5v-109q79 -33 131 -87.5t53 -128.5q1 -46 -15 -84.5t-39 -61t-46 -38t-39 -21.5l-17 -6q6 0 15 -1.5t35 -9t50 -17.5t53 -30t50 -45t35.5 -64t14.5 -84q0 -59 -11.5 -105.5t-28.5 -76.5t-44 -51t-49.5 -31.5t-54.5 -16t-49.5 -6.5 t-43.5 -1v-75q0 -10 -7.5 -17.5t-17.5 -7.5h-150q-10 0 -17.5 7.5t-7.5 17.5v75h-100v-75q0 -10 -7.5 -17.5t-17.5 -7.5h-150q-10 0 -17.5 7.5t-7.5 17.5v75h-175q-10 0 -17.5 7.5t-7.5 17.5v150q0 10 7.5 17.5t17.5 7.5h75v600h-75q-10 0 -17.5 7.5t-7.5 17.5v150 q0 10 7.5 17.5t17.5 7.5h175v75q0 10 7.5 17.5t17.5 7.5h150q10 0 17.5 -7.5t7.5 -17.5v-75h100v75q0 10 7.5 17.5t17.5 7.5zM400 900v-200h263q28 0 48.5 10.5t30 25t15 29t5.5 25.5l1 10q0 4 -0.5 11t-6 24t-15 30t-30 24t-48.5 11h-263zM400 500v-200h363q28 0 48.5 10.5 t30 25t15 29t5.5 25.5l1 10q0 4 -0.5 11t-6 24t-15 30t-30 24t-48.5 11h-363z" />
<glyph unicode="&#xe230;" d="M212 1198h780q86 0 147 -61t61 -147v-416q0 -51 -18 -142.5t-36 -157.5l-18 -66q-29 -87 -93.5 -146.5t-146.5 -59.5h-572q-82 0 -147 59t-93 147q-8 28 -20 73t-32 143.5t-20 149.5v416q0 86 61 147t147 61zM600 1045q-70 0 -132.5 -11.5t-105.5 -30.5t-78.5 -41.5 t-57 -45t-36 -41t-20.5 -30.5l-6 -12l156 -243h560l156 243q-2 5 -6 12.5t-20 29.5t-36.5 42t-57 44.5t-79 42t-105 29.5t-132.5 12zM762 703h-157l195 261z" />
<glyph unicode="&#xe231;" d="M475 1300h150q103 0 189 -86t86 -189v-500q0 -41 -42 -83t-83 -42h-450q-41 0 -83 42t-42 83v500q0 103 86 189t189 86zM700 300v-225q0 -21 -27 -48t-48 -27h-150q-21 0 -48 27t-27 48v225h300z" />
<glyph unicode="&#xe232;" d="M475 1300h96q0 -150 89.5 -239.5t239.5 -89.5v-446q0 -41 -42 -83t-83 -42h-450q-41 0 -83 42t-42 83v500q0 103 86 189t189 86zM700 300v-225q0 -21 -27 -48t-48 -27h-150q-21 0 -48 27t-27 48v225h300z" />
<glyph unicode="&#xe233;" d="M1294 767l-638 -283l-378 170l-78 -60v-224l100 -150v-199l-150 148l-150 -149v200l100 150v250q0 4 -0.5 10.5t0 9.5t1 8t3 8t6.5 6l47 40l-147 65l642 283zM1000 380l-350 -166l-350 166v147l350 -165l350 165v-147z" />
<glyph unicode="&#xe234;" d="M250 800q62 0 106 -44t44 -106t-44 -106t-106 -44t-106 44t-44 106t44 106t106 44zM650 800q62 0 106 -44t44 -106t-44 -106t-106 -44t-106 44t-44 106t44 106t106 44zM1050 800q62 0 106 -44t44 -106t-44 -106t-106 -44t-106 44t-44 106t44 106t106 44z" />
<glyph unicode="&#xe235;" d="M550 1100q62 0 106 -44t44 -106t-44 -106t-106 -44t-106 44t-44 106t44 106t106 44zM550 700q62 0 106 -44t44 -106t-44 -106t-106 -44t-106 44t-44 106t44 106t106 44zM550 300q62 0 106 -44t44 -106t-44 -106t-106 -44t-106 44t-44 106t44 106t106 44z" />
<glyph unicode="&#xe236;" d="M125 1100h950q10 0 17.5 -7.5t7.5 -17.5v-150q0 -10 -7.5 -17.5t-17.5 -7.5h-950q-10 0 -17.5 7.5t-7.5 17.5v150q0 10 7.5 17.5t17.5 7.5zM125 700h950q10 0 17.5 -7.5t7.5 -17.5v-150q0 -10 -7.5 -17.5t-17.5 -7.5h-950q-10 0 -17.5 7.5t-7.5 17.5v150q0 10 7.5 17.5 t17.5 7.5zM125 300h950q10 0 17.5 -7.5t7.5 -17.5v-150q0 -10 -7.5 -17.5t-17.5 -7.5h-950q-10 0 -17.5 7.5t-7.5 17.5v150q0 10 7.5 17.5t17.5 7.5z" />
<glyph unicode="&#xe237;" d="M350 1200h500q162 0 256 -93.5t94 -256.5v-500q0 -165 -93.5 -257.5t-256.5 -92.5h-500q-165 0 -257.5 92.5t-92.5 257.5v500q0 165 92.5 257.5t257.5 92.5zM900 1000h-600q-41 0 -70.5 -29.5t-29.5 -70.5v-600q0 -41 29.5 -70.5t70.5 -29.5h600q41 0 70.5 29.5 t29.5 70.5v600q0 41 -29.5 70.5t-70.5 29.5zM350 900h500q21 0 35.5 -14.5t14.5 -35.5v-300q0 -21 -14.5 -35.5t-35.5 -14.5h-500q-21 0 -35.5 14.5t-14.5 35.5v300q0 21 14.5 35.5t35.5 14.5zM400 800v-200h400v200h-400z" />
<glyph unicode="&#xe238;" d="M150 1100h1000q21 0 35.5 -14.5t14.5 -35.5t-14.5 -35.5t-35.5 -14.5h-50v-200h50q21 0 35.5 -14.5t14.5 -35.5t-14.5 -35.5t-35.5 -14.5h-50v-200h50q21 0 35.5 -14.5t14.5 -35.5t-14.5 -35.5t-35.5 -14.5h-50v-200h50q21 0 35.5 -14.5t14.5 -35.5t-14.5 -35.5 t-35.5 -14.5h-1000q-21 0 -35.5 14.5t-14.5 35.5t14.5 35.5t35.5 14.5h50v200h-50q-21 0 -35.5 14.5t-14.5 35.5t14.5 35.5t35.5 14.5h50v200h-50q-21 0 -35.5 14.5t-14.5 35.5t14.5 35.5t35.5 14.5h50v200h-50q-21 0 -35.5 14.5t-14.5 35.5t14.5 35.5t35.5 14.5z" />
<glyph unicode="&#xe239;" d="M650 1187q87 -67 118.5 -156t0 -178t-118.5 -155q-87 66 -118.5 155t0 178t118.5 156zM300 800q124 0 212 -88t88 -212q-124 0 -212 88t-88 212zM1000 800q0 -124 -88 -212t-212 -88q0 124 88 212t212 88zM300 500q124 0 212 -88t88 -212q-124 0 -212 88t-88 212z M1000 500q0 -124 -88 -212t-212 -88q0 124 88 212t212 88zM700 199v-144q0 -21 -14.5 -35.5t-35.5 -14.5t-35.5 14.5t-14.5 35.5v142q40 -4 43 -4q17 0 57 6z" />
<glyph unicode="&#xe240;" d="M745 878l69 19q25 6 45 -12l298 -295q11 -11 15 -26.5t-2 -30.5q-5 -14 -18 -23.5t-28 -9.5h-8q1 0 1 -13q0 -29 -2 -56t-8.5 -62t-20 -63t-33 -53t-51 -39t-72.5 -14h-146q-184 0 -184 288q0 24 10 47q-20 4 -62 4t-63 -4q11 -24 11 -47q0 -288 -184 -288h-142 q-48 0 -84.5 21t-56 51t-32 71.5t-16 75t-3.5 68.5q0 13 2 13h-7q-15 0 -27.5 9.5t-18.5 23.5q-6 15 -2 30.5t15 25.5l298 296q20 18 46 11l76 -19q20 -5 30.5 -22.5t5.5 -37.5t-22.5 -31t-37.5 -5l-51 12l-182 -193h891l-182 193l-44 -12q-20 -5 -37.5 6t-22.5 31t6 37.5 t31 22.5z" />
<glyph unicode="&#xe241;" d="M1200 900h-50q0 21 -4 37t-9.5 26.5t-18 17.5t-22 11t-28.5 5.5t-31 2t-37 0.5h-200v-850q0 -22 25 -34.5t50 -13.5l25 -2v-100h-400v100q4 0 11 0.5t24 3t30 7t24 15t11 24.5v850h-200q-25 0 -37 -0.5t-31 -2t-28.5 -5.5t-22 -11t-18 -17.5t-9.5 -26.5t-4 -37h-50v300 h1000v-300zM500 450h-25q0 15 -4 24.5t-9 14.5t-17 7.5t-20 3t-25 0.5h-100v-425q0 -11 12.5 -17.5t25.5 -7.5h12v-50h-200v50q50 0 50 25v425h-100q-17 0 -25 -0.5t-20 -3t-17 -7.5t-9 -14.5t-4 -24.5h-25v150h500v-150z" />
<glyph unicode="&#xe242;" d="M1000 300v50q-25 0 -55 32q-14 14 -25 31t-16 27l-4 11l-289 747h-69l-300 -754q-18 -35 -39 -56q-9 -9 -24.5 -18.5t-26.5 -14.5l-11 -5v-50h273v50q-49 0 -78.5 21.5t-11.5 67.5l69 176h293l61 -166q13 -34 -3.5 -66.5t-55.5 -32.5v-50h312zM412 691l134 342l121 -342 h-255zM1100 150v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-1000q-21 0 -35.5 14.5t-14.5 35.5v100q0 21 14.5 35.5t35.5 14.5h1000q21 0 35.5 -14.5t14.5 -35.5z" />
<glyph unicode="&#xe243;" d="M50 1200h1100q21 0 35.5 -14.5t14.5 -35.5v-1100q0 -21 -14.5 -35.5t-35.5 -14.5h-1100q-21 0 -35.5 14.5t-14.5 35.5v1100q0 21 14.5 35.5t35.5 14.5zM611 1118h-70q-13 0 -18 -12l-299 -753q-17 -32 -35 -51q-18 -18 -56 -34q-12 -5 -12 -18v-50q0 -8 5.5 -14t14.5 -6 h273q8 0 14 6t6 14v50q0 8 -6 14t-14 6q-55 0 -71 23q-10 14 0 39l63 163h266l57 -153q11 -31 -6 -55q-12 -17 -36 -17q-8 0 -14 -6t-6 -14v-50q0 -8 6 -14t14 -6h313q8 0 14 6t6 14v50q0 7 -5.5 13t-13.5 7q-17 0 -42 25q-25 27 -40 63h-1l-288 748q-5 12 -19 12zM639 611 h-197l103 264z" />
<glyph unicode="&#xe244;" d="M1200 1100h-1200v100h1200v-100zM50 1000h400q21 0 35.5 -14.5t14.5 -35.5v-900q0 -21 -14.5 -35.5t-35.5 -14.5h-400q-21 0 -35.5 14.5t-14.5 35.5v900q0 21 14.5 35.5t35.5 14.5zM650 1000h400q21 0 35.5 -14.5t14.5 -35.5v-400q0 -21 -14.5 -35.5t-35.5 -14.5h-400 q-21 0 -35.5 14.5t-14.5 35.5v400q0 21 14.5 35.5t35.5 14.5zM700 900v-300h300v300h-300z" />
<glyph unicode="&#xe245;" d="M50 1200h400q21 0 35.5 -14.5t14.5 -35.5v-900q0 -21 -14.5 -35.5t-35.5 -14.5h-400q-21 0 -35.5 14.5t-14.5 35.5v900q0 21 14.5 35.5t35.5 14.5zM650 700h400q21 0 35.5 -14.5t14.5 -35.5v-400q0 -21 -14.5 -35.5t-35.5 -14.5h-400q-21 0 -35.5 14.5t-14.5 35.5v400 q0 21 14.5 35.5t35.5 14.5zM700 600v-300h300v300h-300zM1200 0h-1200v100h1200v-100z" />
<glyph unicode="&#xe246;" d="M50 1000h400q21 0 35.5 -14.5t14.5 -35.5v-350h100v150q0 21 14.5 35.5t35.5 14.5h400q21 0 35.5 -14.5t14.5 -35.5v-150h100v-100h-100v-150q0 -21 -14.5 -35.5t-35.5 -14.5h-400q-21 0 -35.5 14.5t-14.5 35.5v150h-100v-350q0 -21 -14.5 -35.5t-35.5 -14.5h-400 q-21 0 -35.5 14.5t-14.5 35.5v800q0 21 14.5 35.5t35.5 14.5zM700 700v-300h300v300h-300z" />
<glyph unicode="&#xe247;" d="M100 0h-100v1200h100v-1200zM250 1100h400q21 0 35.5 -14.5t14.5 -35.5v-400q0 -21 -14.5 -35.5t-35.5 -14.5h-400q-21 0 -35.5 14.5t-14.5 35.5v400q0 21 14.5 35.5t35.5 14.5zM300 1000v-300h300v300h-300zM250 500h900q21 0 35.5 -14.5t14.5 -35.5v-400 q0 -21 -14.5 -35.5t-35.5 -14.5h-900q-21 0 -35.5 14.5t-14.5 35.5v400q0 21 14.5 35.5t35.5 14.5z" />
<glyph unicode="&#xe248;" d="M600 1100h150q21 0 35.5 -14.5t14.5 -35.5v-400q0 -21 -14.5 -35.5t-35.5 -14.5h-150v-100h450q21 0 35.5 -14.5t14.5 -35.5v-400q0 -21 -14.5 -35.5t-35.5 -14.5h-900q-21 0 -35.5 14.5t-14.5 35.5v400q0 21 14.5 35.5t35.5 14.5h350v100h-150q-21 0 -35.5 14.5 t-14.5 35.5v400q0 21 14.5 35.5t35.5 14.5h150v100h100v-100zM400 1000v-300h300v300h-300z" />
<glyph unicode="&#xe249;" d="M1200 0h-100v1200h100v-1200zM550 1100h400q21 0 35.5 -14.5t14.5 -35.5v-400q0 -21 -14.5 -35.5t-35.5 -14.5h-400q-21 0 -35.5 14.5t-14.5 35.5v400q0 21 14.5 35.5t35.5 14.5zM600 1000v-300h300v300h-300zM50 500h900q21 0 35.5 -14.5t14.5 -35.5v-400 q0 -21 -14.5 -35.5t-35.5 -14.5h-900q-21 0 -35.5 14.5t-14.5 35.5v400q0 21 14.5 35.5t35.5 14.5z" />
<glyph unicode="&#xe250;" d="M865 565l-494 -494q-23 -23 -41 -23q-14 0 -22 13.5t-8 38.5v1000q0 25 8 38.5t22 13.5q18 0 41 -23l494 -494q14 -14 14 -35t-14 -35z" />
<glyph unicode="&#xe251;" d="M335 635l494 494q29 29 50 20.5t21 -49.5v-1000q0 -41 -21 -49.5t-50 20.5l-494 494q-14 14 -14 35t14 35z" />
<glyph unicode="&#xe252;" d="M100 900h1000q41 0 49.5 -21t-20.5 -50l-494 -494q-14 -14 -35 -14t-35 14l-494 494q-29 29 -20.5 50t49.5 21z" />
<glyph unicode="&#xe253;" d="M635 865l494 -494q29 -29 20.5 -50t-49.5 -21h-1000q-41 0 -49.5 21t20.5 50l494 494q14 14 35 14t35 -14z" />
<glyph unicode="&#xe254;" d="M700 741v-182l-692 -323v221l413 193l-413 193v221zM1200 0h-800v200h800v-200z" />
<glyph unicode="&#xe255;" d="M1200 900h-200v-100h200v-100h-300v300h200v100h-200v100h300v-300zM0 700h50q0 21 4 37t9.5 26.5t18 17.5t22 11t28.5 5.5t31 2t37 0.5h100v-550q0 -22 -25 -34.5t-50 -13.5l-25 -2v-100h400v100q-4 0 -11 0.5t-24 3t-30 7t-24 15t-11 24.5v550h100q25 0 37 -0.5t31 -2 t28.5 -5.5t22 -11t18 -17.5t9.5 -26.5t4 -37h50v300h-800v-300z" />
<glyph unicode="&#xe256;" d="M800 700h-50q0 21 -4 37t-9.5 26.5t-18 17.5t-22 11t-28.5 5.5t-31 2t-37 0.5h-100v-550q0 -22 25 -34.5t50 -14.5l25 -1v-100h-400v100q4 0 11 0.5t24 3t30 7t24 15t11 24.5v550h-100q-25 0 -37 -0.5t-31 -2t-28.5 -5.5t-22 -11t-18 -17.5t-9.5 -26.5t-4 -37h-50v300 h800v-300zM1100 200h-200v-100h200v-100h-300v300h200v100h-200v100h300v-300z" />
<glyph unicode="&#xe257;" d="M701 1098h160q16 0 21 -11t-7 -23l-464 -464l464 -464q12 -12 7 -23t-21 -11h-160q-13 0 -23 9l-471 471q-7 8 -7 18t7 18l471 471q10 9 23 9z" />
<glyph unicode="&#xe258;" d="M339 1098h160q13 0 23 -9l471 -471q7 -8 7 -18t-7 -18l-471 -471q-10 -9 -23 -9h-160q-16 0 -21 11t7 23l464 464l-464 464q-12 12 -7 23t21 11z" />
<glyph unicode="&#xe259;" d="M1087 882q11 -5 11 -21v-160q0 -13 -9 -23l-471 -471q-8 -7 -18 -7t-18 7l-471 471q-9 10 -9 23v160q0 16 11 21t23 -7l464 -464l464 464q12 12 23 7z" />
<glyph unicode="&#xe260;" d="M618 993l471 -471q9 -10 9 -23v-160q0 -16 -11 -21t-23 7l-464 464l-464 -464q-12 -12 -23 -7t-11 21v160q0 13 9 23l471 471q8 7 18 7t18 -7z" />
<glyph unicode="&#xf8ff;" d="M1000 1200q0 -124 -88 -212t-212 -88q0 124 88 212t212 88zM450 1000h100q21 0 40 -14t26 -33l79 -194q5 1 16 3q34 6 54 9.5t60 7t65.5 1t61 -10t56.5 -23t42.5 -42t29 -64t5 -92t-19.5 -121.5q-1 -7 -3 -19.5t-11 -50t-20.5 -73t-32.5 -81.5t-46.5 -83t-64 -70 t-82.5 -50q-13 -5 -42 -5t-65.5 2.5t-47.5 2.5q-14 0 -49.5 -3.5t-63 -3.5t-43.5 7q-57 25 -104.5 78.5t-75 111.5t-46.5 112t-26 90l-7 35q-15 63 -18 115t4.5 88.5t26 64t39.5 43.5t52 25.5t58.5 13t62.5 2t59.5 -4.5t55.5 -8l-147 192q-12 18 -5.5 30t27.5 12z" />
<glyph unicode="&#x1f511;" d="M250 1200h600q21 0 35.5 -14.5t14.5 -35.5v-400q0 -21 -14.5 -35.5t-35.5 -14.5h-150v-500l-255 -178q-19 -9 -32 -1t-13 29v650h-150q-21 0 -35.5 14.5t-14.5 35.5v400q0 21 14.5 35.5t35.5 14.5zM400 1100v-100h300v100h-300z" />
<glyph unicode="&#x1f6aa;" d="M250 1200h750q39 0 69.5 -40.5t30.5 -84.5v-933l-700 -117v950l600 125h-700v-1000h-100v1025q0 23 15.5 49t34.5 26zM500 525v-100l100 20v100z" />
</font>
</defs></svg>

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because one or more lines are too long

623
apidoc/assets/main.css Normal file
View File

@@ -0,0 +1,623 @@
/**
* apidoc main css file
*/
/**
* Define colors
*/
:root {
--primary: #0088cc;
--white: #fff;
--light-gray: #ccc;
--main-gray: #777;
--dark-gray: #2d2d2d;
--hover-gray: #666;
--meth-get: green;
--meth-put: #e5c500;
--meth-post: #4070ec;
--meth-delete: #ed0039;
--red: #dc3545;
}
.color-primary {
color: var(--primary);
}
.bg-primary {
background-color: var(--primary);
}
.bg-red {
color: var(--white);
background-color: var(--red);
}
.border-danger {
border: 1px solid var(--red);
}
/** for some reason the iOS safari style is applied on date inputs */
input[type="date"] {
line-height: 1.4 !important;
}
/* ------------------------------------------------------------------------------------------
* Content
* ------------------------------------------------------------------------------------------ */
@font-face {
font-family: 'Glyphicons Halflings';
src: url('./glyphicons-halflings-regular.eot');
src: url('./glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'),
url('./glyphicons-halflings-regular.woff') format('woff'),
url('./glyphicons-halflings-regular.woff2') format('woff2'),
url('./glyphicons-halflings-regular.ttf') format('truetype'),
url('./glyphicons-halflings-regular.svg#glyphicons-halflingsregular') format('svg');
}
/* Hide vertical scrollbar on off canvas animation ("left" positioning) */
html {
overflow-x: hidden;
}
body {
font-family: "Source Sans Pro", sans-serif;
}
a:focus {
background-color: var(--primary);
}
#content {
margin-top: 10px;
padding-left: 10px;
}
p {
font-size: 130%;
color: var(--main-gray);
}
section {
padding: 30px 0;
}
article {
border-top: 1px solid var(--light-gray);
padding: 14px 0 30px 0;
}
table {
border-collapse: collapse;
width: 100%;
margin: 0 0 20px 0;
}
th {
background-color: var(--main-gray);
color: var(--white);
text-align: left;
padding: 5px 8px;
border: 1px solid var(--main-gray);
}
td {
padding: 5px;
border: 1px solid var(--main-gray);
}
td.code {
font-family: "Source Code Pro", monospace;
font-weight: 600;
}
.label {
float: right;
margin-top: 4px;
user-select: none;
}
.label.optional {
background-color: grey;
}
.label.required {
background-color: var(--red);
}
.default-value,
.type-size {
font-style: italic;
font-size: 95%;
}
.open-left {
right: 0;
left: auto;
}
.invisible {
visibility: hidden;
}
.input-group-addon.sample-request-select {
padding: 0 6px;
}
.input-group-addon.sample-request-select select {
width: auto;
height: 32px;
}
.sample-request-input-Boolean-container {
width: 40px;
height: 34px;
background: var(--white);
border: 1px solid var(--light-gray);
}
.sample-request-input-Boolean-container > div {
margin-top: 7px;
text-align: center;
}
.sample-request-input-Boolean-container > div input {
margin: 0;
}
/* ------------------------------------------------------------------------------------------
* Request method (HTTP verb)
* ------------------------------------------------------------------------------------------ */
.method {
font-weight: 600;
font-size: 15px;
display: inline-block;
margin: 0 0 5px 0;
padding: 4px 5px;
border-radius: 6px;
text-transform: uppercase;
background-color: var(--main-gray);
color: var(--white);
}
.meth-get {
background-color: var(--meth-get);
}
.meth-put {
background-color: var(--meth-put);
}
.meth-post {
background-color: var(--meth-post);
}
.meth-delete {
background-color: var(--meth-delete);
}
/* ------------------------------------------------------------------------------------------
* Sidenav
* ------------------------------------------------------------------------------------------ */
#scrollingNav {
position: fixed;
top: 0;
left: 0;
bottom: 0;
z-index: 10;
background-color: var(--dark-gray);
box-shadow: 0 2px 5px 0 rgb(0 0 0 / 16%), 0 2px 10px 0 rgb(0 0 0 / 12%);
}
.sidenav {
color: var(--white);
position: absolute;
top: 50px;
left: 0;
right: 0;
bottom: 0;
overflow-x: hidden;
overflow-y: hidden;
}
.sidenav:hover {
overflow-x: auto;
overflow-y: auto;
}
.sidenav > li > a {
color: var(--white);
display: block;
padding: 8px 13px;
}
/* non active sidenav link are highlighted on hover */
.sidenav > li:not(.active) > a:hover {
background-color: var(--hover-gray);
}
.sidenav > li.nav-header {
margin-top: 8px;
margin-bottom: 8px;
}
.sidenav > li.nav-header > a {
padding: 5px 15px;
font-weight: 700;
font-size: 16px;
background-color: var(--main-gray);
}
.sidenav > li.active > a {
position: relative;
background-color: var(--primary);
color: var(--white);
}
/**
* TODO: commented out for the moment
.sidenav > li.has-modifications a {
border-right: 4px solid var(--main-gray);
}
.nav-list-item :not(.is-new) {
border-left: 4px solid var(--main-gray);
}
.sidenav > li.is-new a {
border-left: 4px solid var(--primary);
}
*/
/*
* Off Canvas
* --------------------------------------------------
*/
@media screen and (max-width: 767px) {
#content {
margin-top: 58px;
}
.row-offcanvas {
position: relative;
-webkit-transition: all .25s ease-out;
-o-transition: all .25s ease-out;
transition: all .25s ease-out;
left: 0;
}
.row-offcanvas,
.row-offcanvas * {
transition: all 0.5s ease-out;
}
.row-offcanvas .sidebar-offcanvas {
position: absolute;
top: 0;
left: -200px !important; /* 6 columns */
width: 100%; /* 6 columns */
max-width: 200px;
}
.nav-toggle {
position: fixed;
left: 0;
background: var(--dark-gray);
width: 100%;
}
.nav-toggle .btn {
margin: 10px 14px;
}
.nav-toggle .icon-bar {
display: block;
width: 22px;
height: 2px;
border-radius: 1px;
background-color: var(--white);
}
.nav-toggle .icon-bar + .icon-bar {
margin-top: 4px;
}
.row-offcanvas.active .sidebar-offcanvas {
left: 0 !important; /* 6 columns */
}
.row-offcanvas.active, .row-offcanvas.active .nav-toggle {
left: 200px;
}
/* Styling the three lines to make it an X */
.row-offcanvas.active .nav-toggle .btn > .icon-bar {
transform: rotate(45deg) translate(-4px, -4px);
}
.row-offcanvas.active .nav-toggle .btn .icon-bar:nth-child(2) {
display: none;
}
.row-offcanvas.active .nav-toggle .btn .icon-bar:nth-child(3) {
transform: rotate(-45deg);
}
}
/* ------------------------------------------------------------------------------------------
* Side nav search
* ------------------------------------------------------------------------------------------ */
.sidenav-search {
padding: 16px 10px 10px;
background-color: var(--dark-gray);
}
.sidenav-search .search {
height: 26px;
}
.search-reset {
position: absolute;
display: block;
cursor: pointer;
width: 20px;
height: 20px;
text-align: center;
right: 28px;
top: 18px;
background-color: #fff;
}
/* ------------------------------------------------------------------------------------------
* Prism - Toolbar
* ------------------------------------------------------------------------------------------ */
div.code-toolbar.code-toolbar > .toolbar {
top: .4rem;
right: .4rem;
}
div.code-toolbar.code-toolbar > .toolbar > .toolbar-item > button:hover,
div.code-toolbar.code-toolbar > .toolbar > .toolbar-item > button:focus {
color: var(--white);
}
div.code-toolbar.code-toolbar > .toolbar > .toolbar-item > button {
color: var(--light-gray);
padding: .5em;
background: var(--hover-gray);
box-shadow: 0 2px 1px 1px rgba(0,0,0,.5);
}
/* ------------------------------------------------------------------------------------------
* Compare
* ------------------------------------------------------------------------------------------ */
ins {
background: #60d060;
text-decoration: none;
color: #000000;
}
del {
background: #f05050;
color: #000000;
}
.label-ins {
background-color: #60d060;
}
.label-del {
background-color: #f05050;
text-decoration: line-through;
}
pre.ins {
background-color: #60d060;
}
pre.del {
background-color: #f05050;
text-decoration: line-through;
}
table.ins th,
table.ins td {
background-color: #60d060;
}
table.del th,
table.del td {
background-color: #f05050;
text-decoration: line-through;
}
tr.ins td {
background-color: #60d060;
}
tr.del td {
background-color: #f05050;
text-decoration: line-through;
}
/* ------------------------------------------------------------------------------------------
* Spinner
* ------------------------------------------------------------------------------------------ */
#loader {
position: absolute;
width: 100%;
}
#loader p {
padding-top: 80px;
margin-left: -4px;
}
.spinner {
margin: 200px auto;
width: 60px;
height: 60px;
position: relative;
}
.container1 > div, .container2 > div, .container3 > div {
width: 14px;
height: 14px;
background-color: #0088cc;
border-radius: 100%;
position: absolute;
-webkit-animation: bouncedelay 1.2s infinite ease-in-out;
animation: bouncedelay 1.2s infinite ease-in-out;
/* Prevent first frame from flickering when animation starts */
-webkit-animation-fill-mode: both;
animation-fill-mode: both;
}
.spinner .spinner-container {
position: absolute;
width: 100%;
height: 100%;
}
.container2 {
-webkit-transform: rotateZ(45deg);
transform: rotateZ(45deg);
}
.container3 {
-webkit-transform: rotateZ(90deg);
transform: rotateZ(90deg);
}
.circle1 { top: 0; left: 0; }
.circle2 { top: 0; right: 0; }
.circle3 { right: 0; bottom: 0; }
.circle4 { left: 0; bottom: 0; }
.container2 .circle1 {
-webkit-animation-delay: -1.1s;
animation-delay: -1.1s;
}
.container3 .circle1 {
-webkit-animation-delay: -1.0s;
animation-delay: -1.0s;
}
.container1 .circle2 {
-webkit-animation-delay: -0.9s;
animation-delay: -0.9s;
}
.container2 .circle2 {
-webkit-animation-delay: -0.8s;
animation-delay: -0.8s;
}
.container3 .circle2 {
-webkit-animation-delay: -0.7s;
animation-delay: -0.7s;
}
.container1 .circle3 {
-webkit-animation-delay: -0.6s;
animation-delay: -0.6s;
}
.container2 .circle3 {
-webkit-animation-delay: -0.5s;
animation-delay: -0.5s;
}
.container3 .circle3 {
-webkit-animation-delay: -0.4s;
animation-delay: -0.4s;
}
.container1 .circle4 {
-webkit-animation-delay: -0.3s;
animation-delay: -0.3s;
}
.container2 .circle4 {
-webkit-animation-delay: -0.2s;
animation-delay: -0.2s;
}
.container3 .circle4 {
-webkit-animation-delay: -0.1s;
animation-delay: -0.1s;
}
@-webkit-keyframes bouncedelay {
0%, 80%, 100% { -webkit-transform: scale(0.0) }
40% { -webkit-transform: scale(1.0) }
}
@keyframes bouncedelay {
0%, 80%, 100% {
transform: scale(0.0);
-webkit-transform: scale(0.0);
} 40% {
transform: scale(1.0);
-webkit-transform: scale(1.0);
}
}
/* ------------------------------------------------------------------------------------------
* Tabs
* ------------------------------------------------------------------------------------------ */
ul.nav-tabs {
margin: 0;
}
p.deprecated span{
color: var(--red);
font-weight: bold;
text-decoration: underline;
}
/**
* Footer
*/
#generator {
padding: 10px 0;
}
/* ------------------------------------------------------------------------------------------
* Print
* ------------------------------------------------------------------------------------------ */
@media print {
#sidenav,
#version,
#versions,
section .version,
section .versions {
display: none;
}
#content {
margin-left: 0;
}
a {
text-decoration: none;
color: inherit;
}
a:after {
content: " [" attr(href) "] ";
}
p {
color: #000000
}
pre {
background-color: #ffffff;
color: #000000;
padding: 10px;
border: #808080 1px solid;
border-radius: 6px;
position: relative;
margin: 10px 0 20px 0;
}
} /* /@media print */

View File

@@ -0,0 +1,13 @@
pre.diff-highlight > code .token.deleted:not(.prefix),
pre > code.diff-highlight .token.deleted:not(.prefix) {
background-color: rgba(255, 0, 0, .1);
color: inherit;
display: block;
}
pre.diff-highlight > code .token.inserted:not(.prefix),
pre > code.diff-highlight .token.inserted:not(.prefix) {
background-color: rgba(0, 255, 128, .1);
color: inherit;
display: block;
}

View File

@@ -0,0 +1,65 @@
div.code-toolbar {
position: relative;
}
div.code-toolbar > .toolbar {
position: absolute;
z-index: 10;
top: .3em;
right: .2em;
transition: opacity 0.3s ease-in-out;
opacity: 0;
}
div.code-toolbar:hover > .toolbar {
opacity: 1;
}
/* Separate line b/c rules are thrown out if selector is invalid.
IE11 and old Edge versions don't support :focus-within. */
div.code-toolbar:focus-within > .toolbar {
opacity: 1;
}
div.code-toolbar > .toolbar > .toolbar-item {
display: inline-block;
}
div.code-toolbar > .toolbar > .toolbar-item > a {
cursor: pointer;
}
div.code-toolbar > .toolbar > .toolbar-item > button {
background: none;
border: 0;
color: inherit;
font: inherit;
line-height: normal;
overflow: visible;
padding: 0;
-webkit-user-select: none; /* for button */
-moz-user-select: none;
-ms-user-select: none;
}
div.code-toolbar > .toolbar > .toolbar-item > a,
div.code-toolbar > .toolbar > .toolbar-item > button,
div.code-toolbar > .toolbar > .toolbar-item > span {
color: #bbb;
font-size: .8em;
padding: 0 .5em;
background: #f5f2f0;
background: rgba(224, 224, 224, 0.2);
box-shadow: 0 2px 0 0 rgba(0,0,0,0.2);
border-radius: .5em;
}
div.code-toolbar > .toolbar > .toolbar-item > a:hover,
div.code-toolbar > .toolbar > .toolbar-item > a:focus,
div.code-toolbar > .toolbar > .toolbar-item > button:hover,
div.code-toolbar > .toolbar > .toolbar-item > button:focus,
div.code-toolbar > .toolbar > .toolbar-item > span:hover,
div.code-toolbar > .toolbar > .toolbar-item > span:focus {
color: inherit;
text-decoration: none;
}

122
apidoc/assets/prism.css Normal file
View File

@@ -0,0 +1,122 @@
/**
* prism.js tomorrow night eighties for JavaScript, CoffeeScript, CSS and HTML
* Based on https://github.com/chriskempson/tomorrow-theme
* @author Rose Pritchard
*/
code[class*="language-"],
pre[class*="language-"] {
color: #ccc;
background: none;
font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
font-size: 1em;
text-align: left;
white-space: pre;
word-spacing: normal;
word-break: normal;
word-wrap: normal;
line-height: 1.5;
-moz-tab-size: 4;
-o-tab-size: 4;
tab-size: 4;
-webkit-hyphens: none;
-moz-hyphens: none;
-ms-hyphens: none;
hyphens: none;
}
/* Code blocks */
pre[class*="language-"] {
padding: 1em;
margin: .5em 0;
overflow: auto;
}
:not(pre) > code[class*="language-"],
pre[class*="language-"] {
background: #2d2d2d;
}
/* Inline code */
:not(pre) > code[class*="language-"] {
padding: .1em;
border-radius: .3em;
white-space: normal;
}
.token.comment,
.token.block-comment,
.token.prolog,
.token.doctype,
.token.cdata {
color: #999;
}
.token.punctuation {
color: #ccc;
}
.token.tag,
.token.attr-name,
.token.namespace,
.token.deleted {
color: #e2777a;
}
.token.function-name {
color: #6196cc;
}
.token.boolean,
.token.number,
.token.function {
color: #f08d49;
}
.token.property,
.token.class-name,
.token.constant,
.token.symbol {
color: #f8c555;
}
.token.selector,
.token.important,
.token.atrule,
.token.keyword,
.token.builtin {
color: #cc99cd;
}
.token.string,
.token.char,
.token.attr-value,
.token.regex,
.token.variable {
color: #7ec699;
}
.token.operator,
.token.entity,
.token.url {
color: #67cdcc;
}
.token.important,
.token.bold {
font-weight: bold;
}
.token.italic {
font-style: italic;
}
.token.entity {
cursor: help;
}
.token.inserted {
color: green;
}

1047
apidoc/index.html Normal file

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More