init
11
.gitignore
vendored
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
node_modules/
|
||||||
|
logs/
|
||||||
|
node_modules.*
|
||||||
|
dist.zip
|
||||||
|
dist/
|
||||||
|
coverage/*
|
||||||
|
admin/node_modules/
|
||||||
|
|
||||||
|
# 临时文件目录(保留目录但忽略文件)
|
||||||
|
temp/*
|
||||||
|
!temp/.gitkeep
|
||||||
18
Dockerfile
Normal 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
@@ -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
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
101
_docs/sql/001_biz_schema.sql
Normal 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(库内仅存 hash;plan_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='月用量';
|
||||||
16
_docs/sql/003_biz_audit.sql
Normal 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='订阅模块审计';
|
||||||
840
_docs/后端管理平台需求说明.md
Normal 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. 管理端API(MVP)
|
||||||
|
|
||||||
|
## 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
|
||||||
|
- status(active / disabled)
|
||||||
|
- created_at
|
||||||
|
- updated_at
|
||||||
|
|
||||||
|
## 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)
|
||||||
|
- created_at
|
||||||
|
- updated_at
|
||||||
|
|
||||||
|
## 3.3 用户套餐实例
|
||||||
|
|
||||||
|
`user_subscriptions`
|
||||||
|
|
||||||
|
- id
|
||||||
|
- user_id
|
||||||
|
- plan_id
|
||||||
|
- start_time
|
||||||
|
- end_time
|
||||||
|
- status(pending/active/expired/cancelled)
|
||||||
|
- payment_channel(offline / pay_link)
|
||||||
|
- payment_ref(线下流水号或第三方订单号)
|
||||||
|
- created_at
|
||||||
|
- updated_at
|
||||||
|
|
||||||
|
## 3.4 Token
|
||||||
|
|
||||||
|
`api_tokens`
|
||||||
|
|
||||||
|
- id
|
||||||
|
- user_id
|
||||||
|
- token_name
|
||||||
|
- token_hash(仅存哈希)
|
||||||
|
- status(active/revoked/expired)
|
||||||
|
- plan_id(冗余,便于鉴权)
|
||||||
|
- expire_at
|
||||||
|
- last_used_at
|
||||||
|
- created_at
|
||||||
|
- updated_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
|
||||||
|
- 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清单(管理端)
|
||||||
|
- 月账单计算任务说明
|
||||||
|
- 上线回滚预案
|
||||||
|
|
||||||
|
---
|
||||||
162
_docs/订阅模块实施计划.md
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
# WechatAdminWeb — 订阅模块后台实施计划
|
||||||
|
|
||||||
|
> 依据:`_docs/后端管理平台需求说明.md` 中 **「订阅模块 / 轻量核心版」** 与 **§7 管理端 API(MVP)**;技术栈以仓库现有 **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 DDL(MySQL)
|
||||||
|
|
||||||
|
新建迁移或 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
@@ -0,0 +1 @@
|
|||||||
|
eyJ2ZXJzaW9uIjoiMy4wIiwiZGF0YSI6eyJyZWdpc3Rlcl9jb2RlIjoiZXlKMklqb2lNeTR4SWl3aWFDSTZJakpsTm1abVpURmhaVEU1WmpnelpXSXlNbVk0T0dFNFpHVm1NVE0zT0Rrd1pERXpZamcwWkdNd05EYzNaall5TURkbU5tSmxNalk1TkRJME1qQTJaVFlpTENKeklqb2lPVEkyT0dRNU1EY3paV0UwTWpnNVpHVXdNRFF6TldOaU5EazBOVFU1TnpraUxDSjBJam94TnpVNU5ERTJNRFUyTENKdUlqb2lNbU00WkRVME1ETTJNR0k1T1RoaE5DSXNJbXNpT2lJMk5EUmpaR0ZoTWlJc0ltTWlPaUptTVdNek5tVmpaREptWmpjMFpXUmpOVFJrTlRrNE5HTTNaV1k1TjJJNFpUQTJaVFV6WVRsbU1Ua3hOV0ZrTURRME5tUTFNRFk0WldSbE5ETmpZMkV6SWl3aWJTSTZJbVF4TVROaFptSXlOREE1T1RsaE5qWWlmUT09IiwidmVyc2lvbiI6IjMuMCIsInJlbWFyayI6IiIsImlzc3VlciI6IlBsYXRmb3JtV2ViIExpY2Vuc2UgU2VydmVyIiwiZW5jcnlwdGVkX2RhdGEiOiJMcXQ1MnpTT2dFdlR6dFA5T0FCTlU1NmY2L3JIMlpNR2puQnA5UXgxb2FkcW1YMXBISThkdEVVWWVET29IQ0Z5Q0N2b2p3TkVpV0R4YmN2RCtMMDAwSitOMXF5WjBNZGdPanZoSGRFbG0wVWxlYVNuNjR6QWU4M1FBelNOUGlIRWJWbEcvSk4ySUpHdVg2anhFaktsZCtCNGVVWXdoa1VVakFsUnVQL0lOVUFZZXplYW1sM0x2RmRTQW4xRnV2QXMiLCJhZXNfaXYiOiJpRkQ0VGhrdEFBcTY3emdsWlR1VStRPT0iLCJub25jZSI6ImRiYjM2ZTg0NThiNTEzMDJkYzNhYTU4NDI1MTY1M2JlIiwia2V5X2lkIjoiNjQ0Y2RhYTItZmRmNy00MjEzLTgyNWItZDkxOWU4YjQ0Mzg4IiwiZGF0YV9oYXNoIjoiYmUzZmFjZDkyNWVjM2FmMjQ3ZjQ3NTZjMzUzMWJlYTM1ZTMyZmZjYTA5MTg0ZDM2Y2E3ZmFmOWJkMWRmYzI3NiIsImV4cGlyZV90aW1lX3B1YmxpYyI6IjIwMjYtMTItMDVUMDg6NTk6MzQuNjQ0WiIsImNyZWF0ZWRfdGltZV9wdWJsaWMiOiIyMDI1LTEyLTA1VDA4OjU5OjM0LjY0NVoifSwiZW5jcnlwdGVkX2tleSI6IlVGZEpzUE5DVW9pb3hNTHE1eFNvaklveHR1c3FkMXhsY0hLODZRS01tT0hkRzZpY3dYd0tDNEx4dDYxTHNMVHY5VEZqN0V5NldGNkVtVjZibDMvK21DKzZsdmRBZnNPL2ZTR0xkcDdmMUhXcC9mUVlqSGJIMEhnZUhjd3ZxUWppajRha2hxdkJsRVN6dWhUZTF2cmxzRDhOZk9aaUlvb0llTHp6aTBoVVpHSVFlTC85ek1ZTjhNQXlkczRkZk9VaGRtTGh0QkVYcHlFR2lkcVRkT28zODh2Z0pKamlLdm9FdU9JNm9tN0NPWFdPNkRvQmxsMkllU0EvVmJzZjE3cm1wbGVyYmtscXlKYktJQXVpZDlFKzNvYms4ZVhHaldpNHVWKzZXNW13anhNMzR5YTBkTEtrcGtlR2J6cXpBTjJmTFJLaWJ3Y0kvU3F2VkU5WnFhdzB6YnlhNHJZczFhVGVWdGp0NXcvQkxwUm9RbzJ2TE9Xdnd1N1R0Nko5U1VRUVNqK1JhVnZzTkQrUmZOdHlmbmd3VlM1ZUdhcEpaRHJOSVlUOHVHZW9qZDRaQjFTdnJyVnh2Ym10YW5EcHYrYkxnZTVPZVNXWCt4M2ErME03Wmd3NE02aTBXRmNoTyticnVFSU9RSkZ4K3g0ZVFoK25oaFFJcnNRaHZoRkpBRXRFTjdhUXludU5yeFNDU1FqdGpnZnhuSnBEOXpTc2w5VmNxanNlZDF5SUJPM3hsUkh4MUdFenFNbG1iTHlLNkpBNlBUcUIwWUJoN0FmM2pkaHZhSDc0b3NRV3RZcjg3dC9oK1dwT2VLL1pDTDJ3WFdKM1Fiam9SVXRMYTFWc3dxZnQweTd4VXBBNzZJMnlaYzdDTHpwdzR0MFl0cjhrQzZLemdhUzhxbkV1cG1vPSIsInNpZ25hdHVyZSI6IlVTNEFCVWwvSm9vZng4MjZzTVBSd2FDOTdscnhaeHJqOWEwT1g3Wm0xSkg0T2lQai9JY1BKVCtUZngyNzZpZkpNRGVwV2pCSFdFbVBWLzRHbDkxZnF3MTAwdk9EejZMQlBMY3doWkFiMzdGZWp0aEwwbnJBZERhZ2NqNUJOYkt6NTREYVJaZzlxNS9EMDlpVlltM2Y1YVFGckJxdS9oRHdFSGFrZGVBelF3eW5KOGxYWVcvYkZLN0ZieU9xakFlbDNMcjNBSTZ1Yzd6dDdmZDNKNHNGTkdqVjdMOWdmSi96eE1tRHdVV3BqdEN3d3RmUmwzNkFkbzl4MzNJbVNERkRvVWlJWWpWeXBUSVhERFlUZGJWSjNhMkEySzgxMUZteG4rRGhqVEJHMm80ZVpkbFJWZnRZcmVwREtUNS9PcjdRK2lxdG0rTEUyVTBNbDBDMXEzeFpaMitKM1RVM3JXTE9LTldIS3hCZk9seFJib0NxclRWcHpoZnhrOCthanRhSGMrWVU1UCsxYUxXN2ZaOEN6dCs5Z1ZMak1MQUI5SEZyRVAyNnFlOTRydDNIOGZtUlR6T3I3bUZsMUZic2dtRkRENkdkdmRuQmUrU09PSGdVQWJQYkI0TTBQcTRlSHJRTWZQSFpRSTRnd3JXY1p4QjgydFd2ZGdWTldHbVlWT0VxVVAxazVhS29wcFI2L2RGZnVmNmZOc3JKc1lWTVhCVWdPODJDWmQ0RzBDUTRDdllFQTVYa0xQUFpKSWlMdHEzQ2Y5eTdxazdidnVSejNMemVMWGhtMUk1ZHVoU2xENjZXNVFLWm9pZlpmYzJGYkwyMVpkV1Y0aGc0UllDRVRidk5MQkpOM092QlpQbTRYZUJmSlZnTGtkc2pXckFocWNVY0dxS243TGttc1U0PSIsInRpbWVzdGFtcCI6MTc2NDkyNTE3NH0=
|
||||||
14
_license/public_key.pem
Normal 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-----
|
||||||
1
_license/registration.code
Normal file
@@ -0,0 +1 @@
|
|||||||
|
eyJ2IjoiMy4xIiwiaCI6IjJlNmZmZTFhZTE5ZjgzZWIyMmY4OGE4ZGVmMTM3ODkwZDEzYjg0ZGMwNDc3ZjYyMDdmNmJlMjY5NDI0MjA2ZTYiLCJzIjoiOTI2OGQ5MDczZWE0Mjg5ZGUwMDQzNWNiNDk0NTU5NzkiLCJ0IjoxNzU5NDE2MDU2LCJuIjoiMmM4ZDU0MDM2MGI5OThhNCIsImsiOiI2NDRjZGFhMiIsImMiOiJmMWMzNmVjZDJmZjc0ZWRjNTRkNTk4NGM3ZWY5N2I4ZTA2ZTUzYTlmMTkxNWFkMDQ0NmQ1MDY4ZWRlNDNjY2EzIiwibSI6ImQxMTNhZmIyNDA5OTlhNjYifQ==
|
||||||
14
admin/.babelrc
Normal 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
@@ -0,0 +1,2 @@
|
|||||||
|
# 生产环境:与 config/index.js 中 productionConfig 对应,按需修改域名
|
||||||
|
BUILD_ENV=prod
|
||||||
2
admin/.env.sit
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
# SIT 环境:与 config/index.js 中 sitConfig 对应,按需修改域名
|
||||||
|
BUILD_ENV=sit
|
||||||
24
admin/.gitignore
vendored
Normal 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
@@ -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
@@ -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
|
||||||
|
|
||||||
9
admin/config/config.example.js
Normal 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
@@ -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
39
admin/package.json
Normal 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
@@ -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>
|
||||||
21
admin/src/api/ad/sysAdServer.js
Normal 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;
|
||||||
11
admin/src/api/biz/biz_payment_server.js
Normal 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();
|
||||||
27
admin/src/api/biz/biz_plan_server.js
Normal 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();
|
||||||
27
admin/src/api/biz/biz_subscription_server.js
Normal 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();
|
||||||
15
admin/src/api/biz/biz_token_server.js
Normal 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();
|
||||||
27
admin/src/api/biz/biz_user_server.js
Normal 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();
|
||||||
33
admin/src/api/resources/sys_file_server.js
Normal 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
|
||||||
|
})
|
||||||
|
}
|
||||||
36
admin/src/api/system/fileServe.js
Normal 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;
|
||||||
30
admin/src/api/system/rolePermissionServer.js
Normal 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
|
||||||
26
admin/src/api/system/roleServer.js
Normal 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;
|
||||||
10
admin/src/api/system/sysAddressServer.js
Normal 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;
|
||||||
30
admin/src/api/system/sysModuleServer.js
Normal 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;
|
||||||
30
admin/src/api/system/sys_log_serve.js
Normal 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;
|
||||||
38
admin/src/api/system/systemType_server.js
Normal 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;
|
||||||
|
|
||||||
31
admin/src/api/system/tableServer.js
Normal 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
|
||||||
40
admin/src/api/system/userServer.js
Normal 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;
|
||||||
66
admin/src/components/CustomTabPane.vue
Normal 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>
|
||||||
|
|
||||||
118
admin/src/components/CustomTabs.vue
Normal 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>
|
||||||
|
|
||||||
32
admin/src/framework/admin-framework.js
Normal file
39
admin/src/main.js
Normal 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')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
18
admin/src/router/component-map.js
Normal 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;
|
||||||
96
admin/src/views/biz/biz_payment.vue
Normal 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>
|
||||||
258
admin/src/views/biz/biz_plans.vue
Normal 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>
|
||||||
280
admin/src/views/biz/biz_subscriptions.vue
Normal 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>
|
||||||
193
admin/src/views/biz/biz_tokens.vue
Normal 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>
|
||||||
270
admin/src/views/biz/biz_users.vue
Normal 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>
|
||||||
35
admin/src/views/test/test.vue
Normal 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
@@ -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 时加载 .env(build:sit / build:prod),否则 dev 用 development,build 用 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
15
api/controller_admin/biz_audit_log.js
Normal 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);
|
||||||
|
},
|
||||||
|
};
|
||||||
8
api/controller_admin/biz_dashboard.js
Normal 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);
|
||||||
|
},
|
||||||
|
};
|
||||||
24
api/controller_admin/biz_payment.js
Normal 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);
|
||||||
|
},
|
||||||
|
};
|
||||||
77
api/controller_admin/biz_plan.js
Normal 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);
|
||||||
|
},
|
||||||
|
};
|
||||||
56
api/controller_admin/biz_subscription.js
Normal 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);
|
||||||
|
},
|
||||||
|
};
|
||||||
34
api/controller_admin/biz_token.js
Normal 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
|
||||||
35
api/controller_admin/biz_usage.js
Normal 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);
|
||||||
|
},
|
||||||
|
};
|
||||||
109
api/controller_admin/biz_user.js
Normal 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 });
|
||||||
|
},
|
||||||
|
};
|
||||||
49
api/controller_admin/sys_file.js
Normal 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();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
19
api/controller_front/auth_verify.js
Normal 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);
|
||||||
|
},
|
||||||
|
};
|
||||||
0
api/controller_front/sys_user.js
Normal file
46
api/model/biz_api_token.js
Normal 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",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
54
api/model/biz_audit_log.js
Normal 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
@@ -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: "套餐",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
48
api/model/biz_subscription.js
Normal 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: "订阅",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
38
api/model/biz_usage_monthly.js
Normal 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
@@ -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: "业务用户",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
26
api/model/sys_control_type.js
Normal 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
@@ -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
@@ -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: "菜单类型",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
37
api/model/sys_parameter.js
Normal 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
@@ -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
@@ -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",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
189
api/service/biz_admin_crud.js
Normal 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,
|
||||||
|
};
|
||||||
37
api/service/biz_audit_service.js
Normal 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 };
|
||||||
107
api/service/biz_auth_verify.js
Normal 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 };
|
||||||
47
api/service/biz_dashboard_service.js
Normal 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 };
|
||||||
130
api/service/biz_subscription_logic.js
Normal 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,
|
||||||
|
};
|
||||||
90
api/service/biz_token_logic.js
Normal 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,
|
||||||
|
};
|
||||||
110
api/service/biz_usage_service.js
Normal 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
@@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用 ossPath(https)作为 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
|
||||||
|
// 使用 ossPath(https)作为 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
|
||||||
BIN
apidoc/assets/android-chrome-192x192.png
Normal file
|
After Width: | Height: | Size: 7.5 KiB |
BIN
apidoc/assets/android-chrome-512x512.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
apidoc/assets/apple-touch-icon.png
Normal file
|
After Width: | Height: | Size: 8.6 KiB |
6
apidoc/assets/bootstrap.min.css
vendored
Normal file
1
apidoc/assets/bootstrap.min.css.map
Normal file
BIN
apidoc/assets/favicon-16x16.png
Normal file
|
After Width: | Height: | Size: 566 B |
BIN
apidoc/assets/favicon-32x32.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
apidoc/assets/favicon.ico
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
apidoc/assets/glyphicons-halflings-regular.eot
Normal file
288
apidoc/assets/glyphicons-halflings-regular.svg
Normal 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=" " />
|
||||||
|
<glyph unicode="¥" 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=" " horiz-adv-x="650" />
|
||||||
|
<glyph unicode=" " horiz-adv-x="1300" />
|
||||||
|
<glyph unicode=" " horiz-adv-x="650" />
|
||||||
|
<glyph unicode=" " horiz-adv-x="1300" />
|
||||||
|
<glyph unicode=" " horiz-adv-x="433" />
|
||||||
|
<glyph unicode=" " horiz-adv-x="325" />
|
||||||
|
<glyph unicode=" " horiz-adv-x="216" />
|
||||||
|
<glyph unicode=" " horiz-adv-x="216" />
|
||||||
|
<glyph unicode=" " horiz-adv-x="162" />
|
||||||
|
<glyph unicode=" " horiz-adv-x="260" />
|
||||||
|
<glyph unicode=" " horiz-adv-x="72" />
|
||||||
|
<glyph unicode=" " horiz-adv-x="260" />
|
||||||
|
<glyph unicode=" " horiz-adv-x="325" />
|
||||||
|
<glyph unicode="€" 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="₽" 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="−" 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="⌛" 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="◼" horiz-adv-x="500" d="M0 0z" />
|
||||||
|
<glyph unicode="☁" 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="⛺" 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="✉" 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="✏" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" d="M1300 0h-538l-41 400h-242l-41 -400h-538l431 1200h209l-21 -300h162l-20 300h208zM515 800l-27 -300h224l-27 300h-170z" />
|
||||||
|
<glyph unicode="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" d="M885 900l-352 -353l352 -353l-197 -198l-552 552l552 550z" />
|
||||||
|
<glyph unicode="" d="M1064 547l-551 -551l-198 198l353 353l-353 353l198 198z" />
|
||||||
|
<glyph unicode="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" d="M1100 411l-198 -199l-353 353l-353 -353l-197 199l551 551z" />
|
||||||
|
<glyph unicode="" d="M1101 789l-550 -551l-551 551l198 199l353 -353l353 353z" />
|
||||||
|
<glyph unicode="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" d="M1200 103l-483 276l-314 -399v423h-399l1196 796v-1096zM483 424v-230l683 953z" />
|
||||||
|
<glyph unicode="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" d="M700 741v-182l-692 -323v221l413 193l-413 193v221zM1200 0h-800v200h800v-200z" />
|
||||||
|
<glyph unicode="" 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="" 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="" 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="" 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="" 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="" 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="" 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="🔑" 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="🚪" 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 |
BIN
apidoc/assets/glyphicons-halflings-regular.ttf
Normal file
BIN
apidoc/assets/glyphicons-halflings-regular.woff
Normal file
BIN
apidoc/assets/glyphicons-halflings-regular.woff2
Normal file
92
apidoc/assets/main.bundle.js
Normal file
623
apidoc/assets/main.css
Normal 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 */
|
||||||
13
apidoc/assets/prism-diff-highlight.css
Normal 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;
|
||||||
|
}
|
||||||
65
apidoc/assets/prism-toolbar.css
Normal 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
@@ -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;
|
||||||
|
}
|
||||||