fix(web): 本地优先保存与 design 页体验修复
- 保存先写本地缓存,登录后可选云端同步;退出登录不再因文件操作跳转登录 - 修复 Layui 遮罩残留、design2 保存后线条、toast 被 finally 提前关闭 - 保存过程恢复 loading spinner;成功提示 2.5 秒 - 云端 payload 瘦身与体积超限提示;后端 schema 迁移与 FileService 容错 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -17,7 +17,7 @@ alwaysApply: true
|
|||||||
- **继承上下文**:结合整段对话;最新一条默认延续当前任务,区分细化与换方向。
|
- **继承上下文**:结合整段对话;最新一条默认延续当前任务,区分细化与换方向。
|
||||||
- **回复**:简体中文;标识符与代码保持原文。结构:**结论** → **要点** → **命令**(若有)。少寒暄、少加粗;禁「保证/一定/完美/彻底解决」。
|
- **回复**:简体中文;标识符与代码保持原文。结构:**结论** → **要点** → **命令**(若有)。少寒暄、少加粗;禁「保证/一定/完美/彻底解决」。
|
||||||
- **引用代码**:独占一行 `startLine:endLine:filepath`,行号与路径须真实。
|
- **引用代码**:独占一行 `startLine:endLine:filepath`,行号与路径须真实。
|
||||||
- **中文 UTF-8**:含中文文件 UTF-8 无 BOM;灌库用 `mysql` 导入 `backend-web/schema.sql`(禁 `type`/管道灌库);`bat` 不写中文 SQL。
|
- **中文 UTF-8**:含中文文件 UTF-8 无 BOM;灌库在 `backend-web` 目录执行 `mysql ... < database/schema.sql`(禁 `type`/管道灌库);`bat` 不写中文 SQL。
|
||||||
- **本仓库配置**:后端 `backend-web/config/local.php`;前端 `frontend-web/config/local.js`(gitignore,全量更新勿覆盖;页面直接引该文件,设置 `SOON_DEPLOY_CONFIG`)。
|
- **本仓库配置**:后端 `backend-web/config/local.php`;前端 `frontend-web/config/local.js`(gitignore,全量更新勿覆盖;页面直接引该文件,设置 `SOON_DEPLOY_CONFIG`)。
|
||||||
|
|
||||||
## 改动
|
## 改动
|
||||||
|
|||||||
@@ -25,3 +25,4 @@ backend-web/storage/templates/*
|
|||||||
*.pem
|
*.pem
|
||||||
*.key
|
*.key
|
||||||
/soonDesign-back
|
/soonDesign-back
|
||||||
|
/info
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
|---|---|
|
|---|---|
|
||||||
| [frontend-electron/](frontend-electron/) | 桌面程序 |
|
| [frontend-electron/](frontend-electron/) | 桌面程序 |
|
||||||
| [frontend-web/](frontend-web/) | Web 前端 |
|
| [frontend-web/](frontend-web/) | Web 前端 |
|
||||||
| [backend-web/](backend-web/) | PHP API |
|
| [backend-web/](backend-web/) | PHP API(含 `database/schema.sql`) |
|
||||||
| [docker/](docker/) | 本地 Docker 编排(非生产部署) |
|
| [docker/](docker/) | 本地 Docker 编排(非生产部署) |
|
||||||
|
|
||||||
## 本地开发
|
## 本地开发
|
||||||
@@ -39,7 +39,7 @@ scripts\start.bat
|
|||||||
|
|
||||||
- Web 根目录:`frontend-web/`
|
- Web 根目录:`frontend-web/`
|
||||||
- PHP 根目录:`backend-web/public/`
|
- PHP 根目录:`backend-web/public/`
|
||||||
- 数据库:导入 `backend-web/schema.sql`
|
- 数据库:在 `backend-web` 目录执行 `mysql ... < database/schema.sql`;已有库 `php scripts/migrate-schema.php`
|
||||||
- 后端:`backend-web/config/local.php.example` → `local.php`
|
- 后端:`backend-web/config/local.php.example` → `local.php`
|
||||||
- 前端 API:`frontend-web/config/local.js`(生产配置,勿覆盖)
|
- 前端 API:`frontend-web/config/local.js`(生产配置,勿覆盖)
|
||||||
|
|
||||||
|
|||||||
@@ -33,10 +33,26 @@ vi config/local.php
|
|||||||
改 `db`、`jwt_secret`、`site`、`storage` 路径。
|
改 `db`、`jwt_secret`、`site`、`storage` 路径。
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
mysql -u root -p soondesign < schema.sql
|
mysql -u root -p soondesign < database/schema.sql
|
||||||
|
php scripts/migrate-schema.php
|
||||||
```
|
```
|
||||||
|
|
||||||
再执行上面「权限」整段粘贴。
|
`migrate-schema.php` 用于已有库增量升级(可重复执行)。再执行上面「权限」整段粘贴。
|
||||||
|
|
||||||
|
### MySQL 大包(云端保存失败时)
|
||||||
|
|
||||||
|
若保存报「文件过大」或 `max_allowed_packet`,在宝塔 / MySQL 配置中把 `max_allowed_packet` 调到 **32M** 或以上,重启 MySQL 后生效。可临时验证:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SET GLOBAL max_allowed_packet = 33554432;
|
||||||
|
```
|
||||||
|
|
||||||
|
持久化写入 `my.cnf`(示例):
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[mysqld]
|
||||||
|
max_allowed_packet = 32M
|
||||||
|
```
|
||||||
|
|
||||||
## 初始化管理员
|
## 初始化管理员
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Incremental DB migration (reads config/local.php). Safe to run repeatedly.
|
||||||
|
* Usage: cd backend-web && php scripts/migrate-schema.php
|
||||||
|
*/
|
||||||
|
if (PHP_SAPI !== 'cli') {
|
||||||
|
fwrite(STDERR, "CLI only\n");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
require dirname(__DIR__) . '/src/bootstrap.php';
|
||||||
|
require __DIR__ . '/schema-migrate-lib.php';
|
||||||
|
|
||||||
|
use Soon\Api\Core\Db;
|
||||||
|
|
||||||
|
try {
|
||||||
|
$count = soon_schema_migrate(Db::pdo());
|
||||||
|
fwrite(STDOUT, 'migrate-schema: done (' . $count . " ALTER/CREATE executed)\n");
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
fwrite(STDERR, 'migrate-schema: ' . $e->getMessage() . PHP_EOL);
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Incremental schema patches (safe to run repeatedly).
|
||||||
|
*
|
||||||
|
* @param-out int $applied
|
||||||
|
*/
|
||||||
|
function soon_schema_column_exists(PDO $pdo, string $table, string $column): bool
|
||||||
|
{
|
||||||
|
$stmt = $pdo->prepare(
|
||||||
|
'SELECT COUNT(*) FROM information_schema.COLUMNS '
|
||||||
|
. 'WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = :t AND COLUMN_NAME = :c'
|
||||||
|
);
|
||||||
|
$stmt->execute(['t' => $table, 'c' => $column]);
|
||||||
|
return (int)$stmt->fetchColumn() > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return int Number of SQL statements executed */
|
||||||
|
function soon_schema_migrate(PDO $pdo, string $logPrefix = 'migrate-schema'): int
|
||||||
|
{
|
||||||
|
$applied = 0;
|
||||||
|
$run = static function (string $sql) use ($pdo, $logPrefix, &$applied): void {
|
||||||
|
$pdo->exec($sql);
|
||||||
|
++$applied;
|
||||||
|
fwrite(STDOUT, "{$logPrefix}: {$sql}" . PHP_EOL);
|
||||||
|
};
|
||||||
|
|
||||||
|
$alters = [];
|
||||||
|
if (!soon_schema_column_exists($pdo, 'subscriptions', 'source_order_id')) {
|
||||||
|
$alters[] = 'ALTER TABLE subscriptions ADD COLUMN source_order_id BIGINT UNSIGNED DEFAULT NULL AFTER plan_id';
|
||||||
|
$alters[] = 'ALTER TABLE subscriptions ADD KEY source_order (source_order_id)';
|
||||||
|
}
|
||||||
|
if (!soon_schema_column_exists($pdo, 'pay_orders', 'refund_status')) {
|
||||||
|
$alters[] = "ALTER TABLE pay_orders ADD COLUMN refund_status ENUM('none','pending','approved','rejected') NOT NULL DEFAULT 'none' AFTER status";
|
||||||
|
$alters[] = 'ALTER TABLE pay_orders ADD COLUMN refund_reason TEXT DEFAULT NULL AFTER refund_status';
|
||||||
|
$alters[] = 'ALTER TABLE pay_orders ADD COLUMN refund_note TEXT DEFAULT NULL AFTER refund_reason';
|
||||||
|
$alters[] = 'ALTER TABLE pay_orders ADD COLUMN cancelled_at DATETIME DEFAULT NULL AFTER paid_at';
|
||||||
|
$alters[] = 'ALTER TABLE pay_orders ADD COLUMN refunded_at DATETIME DEFAULT NULL AFTER cancelled_at';
|
||||||
|
$alters[] = 'ALTER TABLE pay_orders ADD KEY refund_status (refund_status)';
|
||||||
|
}
|
||||||
|
if (!soon_schema_column_exists($pdo, 'plans', 'description')) {
|
||||||
|
$alters[] = 'ALTER TABLE plans ADD COLUMN description VARCHAR(255) DEFAULT NULL AFTER name';
|
||||||
|
$alters[] = 'ALTER TABLE plans ADD COLUMN max_files INT UNSIGNED NOT NULL DEFAULT 0 AFTER quota_mb';
|
||||||
|
$alters[] = 'ALTER TABLE plans ADD COLUMN features TEXT DEFAULT NULL AFTER duration_days';
|
||||||
|
$alters[] = 'ALTER TABLE plans ADD COLUMN sort_order INT NOT NULL DEFAULT 0 AFTER features';
|
||||||
|
$alters[] = 'ALTER TABLE plans ADD COLUMN is_recommended TINYINT(1) NOT NULL DEFAULT 0 AFTER sort_order';
|
||||||
|
}
|
||||||
|
if (!soon_schema_column_exists($pdo, 'users', 'admin_level')) {
|
||||||
|
$alters[] = "ALTER TABLE users ADD COLUMN admin_level ENUM('full','ops') DEFAULT NULL AFTER role";
|
||||||
|
}
|
||||||
|
if (!soon_schema_column_exists($pdo, 'soon_files', 'thumb')) {
|
||||||
|
$alters[] = 'ALTER TABLE soon_files ADD COLUMN thumb MEDIUMTEXT DEFAULT NULL AFTER version';
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($alters as $sql) {
|
||||||
|
$run($sql);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (soon_schema_column_exists($pdo, 'users', 'admin_level')) {
|
||||||
|
$pdo->exec(
|
||||||
|
"UPDATE users SET admin_level = 'full' WHERE role = 'admin' AND (admin_level IS NULL OR admin_level = '')"
|
||||||
|
);
|
||||||
|
fwrite(STDOUT, "{$logPrefix}: admin_level backfill for existing admins" . PHP_EOL);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (soon_schema_column_exists($pdo, 'plans', 'description')) {
|
||||||
|
$legacyCodes = ['pro_monthly' => 'member_monthly', 'pro_yearly' => 'member_yearly'];
|
||||||
|
foreach ($legacyCodes as $oldCode => $newCode) {
|
||||||
|
$stmt = $pdo->prepare('SELECT id FROM plans WHERE code = :c LIMIT 1');
|
||||||
|
$stmt->execute(['c' => $oldCode]);
|
||||||
|
if ($stmt->fetch()) {
|
||||||
|
$conflict = $pdo->prepare('SELECT id FROM plans WHERE code = :c LIMIT 1');
|
||||||
|
$conflict->execute(['c' => $newCode]);
|
||||||
|
if ($conflict->fetch()) {
|
||||||
|
$pdo->prepare('UPDATE plans SET is_active = 0 WHERE code = :c')->execute(['c' => $oldCode]);
|
||||||
|
} else {
|
||||||
|
$pdo->prepare('UPDATE plans SET code = :new WHERE code = :old')->execute(['new' => $newCode, 'old' => $oldCode]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$quota = 2048;
|
||||||
|
$maxFiles = 200;
|
||||||
|
$seed = [
|
||||||
|
['free', '免费版', '免费体验设计与编辑', 0, $quota, $maxFiles, 0,
|
||||||
|
'["设计与编辑工具免费使用"]', 0, 0, 1],
|
||||||
|
['member_lifetime', '永久会员', '一次激活,永久使用预览交付能力', 100, $quota, $maxFiles, 0,
|
||||||
|
'["高清预览","成品打印","云端保存","导出设计文件","永久有效"]', 10, 1, 1],
|
||||||
|
['member_monthly', '月度订阅', '按月灵活使用,随时续订', 1999, $quota, $maxFiles, 30,
|
||||||
|
'["高清预览","成品打印","云端保存","导出设计文件","订阅周期:1 个月"]', 20, 0, 0],
|
||||||
|
['member_quarterly', '季度订阅', '连续三个月,更省心', 5299, $quota, $maxFiles, 90,
|
||||||
|
'["高清预览","成品打印","云端保存","导出设计文件","订阅周期:1 季"]', 25, 0, 0],
|
||||||
|
['member_yearly', '年度订阅', '全年畅享,性价比更高', 19999, $quota, $maxFiles, 365,
|
||||||
|
'["高清预览","成品打印","云端保存","导出设计文件","订阅周期:1 年"]', 30, 0, 0],
|
||||||
|
];
|
||||||
|
$upsert = $pdo->prepare(
|
||||||
|
'INSERT INTO plans (code, name, description, price_cents, quota_mb, max_files, duration_days, features, sort_order, is_recommended, is_active) '
|
||||||
|
. 'VALUES (:c, :n, :d, :p, :q, :mf, :dd, :f, :so, :ir, :a) '
|
||||||
|
. 'ON DUPLICATE KEY UPDATE name=VALUES(name), description=VALUES(description), price_cents=VALUES(price_cents), '
|
||||||
|
. 'quota_mb=VALUES(quota_mb), max_files=VALUES(max_files), duration_days=VALUES(duration_days), '
|
||||||
|
. 'features=VALUES(features), sort_order=VALUES(sort_order), is_recommended=VALUES(is_recommended), is_active=VALUES(is_active)'
|
||||||
|
);
|
||||||
|
foreach ($seed as $row) {
|
||||||
|
$upsert->execute([
|
||||||
|
'c' => $row[0], 'n' => $row[1], 'd' => $row[2], 'p' => $row[3], 'q' => $row[4],
|
||||||
|
'mf' => $row[5], 'dd' => $row[6], 'f' => $row[7], 'so' => $row[8], 'ir' => $row[9], 'a' => $row[10],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
fwrite(STDOUT, "{$logPrefix}: membership plans catalog synced" . PHP_EOL);
|
||||||
|
|
||||||
|
$legacyFeatStmt = $pdo->query(
|
||||||
|
'SELECT code, duration_days FROM plans WHERE features LIKE \'%预览成品效果%\' '
|
||||||
|
. 'OR features LIKE \'%到期后恢复为非会员%\' OR features LIKE \'%会员权益一致%\' '
|
||||||
|
. 'OR name LIKE \'%会员 ·%\''
|
||||||
|
);
|
||||||
|
$featUpd = $pdo->prepare('UPDATE plans SET features = :f WHERE code = :c');
|
||||||
|
$coreFeatures = ['高清预览', '成品打印', '云端保存', '导出设计文件'];
|
||||||
|
foreach ($legacyFeatStmt->fetchAll() as $legacyRow) {
|
||||||
|
$code = (string)$legacyRow['code'];
|
||||||
|
$days = (int)$legacyRow['duration_days'];
|
||||||
|
if ($code === 'free') {
|
||||||
|
$featUpd->execute([
|
||||||
|
'f' => '["设计与编辑工具免费使用"]',
|
||||||
|
'c' => $code,
|
||||||
|
]);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$lines = $coreFeatures;
|
||||||
|
if ($days > 0) {
|
||||||
|
$period = $days >= 365 ? '1 年' : ($days === 90 ? '1 季' : ($days >= 30 && $days % 30 === 0 ? ((int)($days / 30)) . ' 个月' : $days . ' 天'));
|
||||||
|
$lines[] = '订阅周期:' . $period;
|
||||||
|
}
|
||||||
|
$featUpd->execute([
|
||||||
|
'f' => json_encode($lines, JSON_UNESCAPED_UNICODE),
|
||||||
|
'c' => $code,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
fwrite(STDOUT, "{$logPrefix}: legacy plan copy refreshed" . PHP_EOL);
|
||||||
|
}
|
||||||
|
|
||||||
|
$run(
|
||||||
|
'CREATE TABLE IF NOT EXISTS `soon_templates` ('
|
||||||
|
. '`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,'
|
||||||
|
. '`name` VARCHAR(120) NOT NULL,'
|
||||||
|
. '`type` TINYINT UNSIGNED NOT NULL DEFAULT 1,'
|
||||||
|
. '`thumb` MEDIUMTEXT DEFAULT NULL,'
|
||||||
|
. '`file_size` INT UNSIGNED NOT NULL DEFAULT 0,'
|
||||||
|
. '`sort_order` INT NOT NULL DEFAULT 0,'
|
||||||
|
. '`is_active` TINYINT(1) NOT NULL DEFAULT 1,'
|
||||||
|
. '`created_at` DATETIME NOT NULL,'
|
||||||
|
. '`updated_at` DATETIME NOT NULL,'
|
||||||
|
. 'PRIMARY KEY (`id`),'
|
||||||
|
. 'KEY `active_sort` (`is_active`, `sort_order`, `id`)'
|
||||||
|
. ') ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci'
|
||||||
|
);
|
||||||
|
fwrite(STDOUT, "{$logPrefix}: soon_templates ensured" . PHP_EOL);
|
||||||
|
|
||||||
|
return $applied;
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Soon\Api\Core;
|
||||||
|
|
||||||
|
use PDOException;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API 未捕获异常 → JSON 响应(避免空白 500)。
|
||||||
|
*/
|
||||||
|
final class ErrorHandler
|
||||||
|
{
|
||||||
|
public static function register(): void
|
||||||
|
{
|
||||||
|
set_exception_handler([self::class, 'handleException']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function handleException(Throwable $e): void
|
||||||
|
{
|
||||||
|
$path = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH) ?: '/';
|
||||||
|
if (!str_starts_with($path, '/api/')) {
|
||||||
|
http_response_code(500);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
$debug = (bool)Config::get('app.debug', false);
|
||||||
|
$code = 'server_error';
|
||||||
|
$msg = '服务器错误,请稍后重试';
|
||||||
|
$status = 500;
|
||||||
|
|
||||||
|
if ($e instanceof PDOException) {
|
||||||
|
$msg = '数据库操作失败';
|
||||||
|
$sqlState = (string)$e->getCode();
|
||||||
|
$detail = $e->getMessage();
|
||||||
|
if (str_contains($detail, 'max_allowed_packet') || str_contains($detail, 'Packet too large')) {
|
||||||
|
$code = 'payload_too_large';
|
||||||
|
$msg = '文件过大,超出服务器数据库限制';
|
||||||
|
$status = 413;
|
||||||
|
} elseif (str_contains($detail, 'Unknown column') || str_contains($detail, "doesn't exist")) {
|
||||||
|
$msg = '数据库结构不完整,请导入 database/schema.sql 或执行 php scripts/migrate-schema.php';
|
||||||
|
} elseif ($sqlState === '22001' || str_contains($detail, 'Data too long')) {
|
||||||
|
$code = 'payload_too_large';
|
||||||
|
$msg = '文件内容过长,无法保存';
|
||||||
|
$status = 413;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($debug) {
|
||||||
|
$msg .= ' (' . $e->getMessage() . ')';
|
||||||
|
}
|
||||||
|
Json::fail($code, $msg, $status);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,12 +6,16 @@ namespace Soon\Api\Services;
|
|||||||
use Soon\Api\Core\Config;
|
use Soon\Api\Core\Config;
|
||||||
use Soon\Api\Core\Db;
|
use Soon\Api\Core\Db;
|
||||||
use Soon\Api\Core\Json;
|
use Soon\Api\Core\Json;
|
||||||
|
use PDOException;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* .soon 文件服务:JSON 数据流模型,乐观锁、配额、软删除。
|
* .soon 文件服务:JSON 数据流模型,乐观锁、配额、软删除。
|
||||||
*/
|
*/
|
||||||
final class FileService
|
final class FileService
|
||||||
{
|
{
|
||||||
|
private const MAX_JSON_BYTES = 33554432;
|
||||||
|
|
||||||
public static function usersRoot(): string
|
public static function usersRoot(): string
|
||||||
{
|
{
|
||||||
$dir = (string)Config::get('storage.users_dir', SOON_SERVER_ROOT . '/storage/users');
|
$dir = (string)Config::get('storage.users_dir', SOON_SERVER_ROOT . '/storage/users');
|
||||||
@@ -74,32 +78,40 @@ final class FileService
|
|||||||
|
|
||||||
public static function create(int $userId, string $name, string $json): array
|
public static function create(int $userId, string $name, string $json): array
|
||||||
{
|
{
|
||||||
$maxFiles = MembershipService::maxFilesLimit($userId);
|
$name = self::normalizeName($name);
|
||||||
if (self::fileCount($userId) >= $maxFiles) {
|
self::assertJsonPayload($json);
|
||||||
Json::fail('file_limit_exceeded', '已达文件数量上限(' . $maxFiles . ' 个),请清理文件或续订', 413);
|
try {
|
||||||
|
$maxFiles = MembershipService::maxFilesLimit($userId);
|
||||||
|
if (self::fileCount($userId) >= $maxFiles) {
|
||||||
|
Json::fail('file_limit_exceeded', '已达文件数量上限(' . $maxFiles . ' 个),请清理文件或续订', 413);
|
||||||
|
}
|
||||||
|
$size = strlen($json);
|
||||||
|
$quota = self::quotaBytes($userId);
|
||||||
|
$used = self::usedBytes($userId);
|
||||||
|
if ($quota > 0 && $used + $size > $quota) {
|
||||||
|
Json::fail('quota_exceeded', '存储空间已满,请清理文件或续订', 413);
|
||||||
|
}
|
||||||
|
$now = date('Y-m-d H:i:s');
|
||||||
|
$thumb = self::extractThumb($json);
|
||||||
|
$stmt = Db::pdo()->prepare(
|
||||||
|
'INSERT INTO soon_files (user_id, name, json, size, version, thumb, created_at, updated_at) '
|
||||||
|
. 'VALUES (:u, :n, :j, :s, 1, :th, :created_at, :updated_at)'
|
||||||
|
);
|
||||||
|
$stmt->execute([
|
||||||
|
'u' => $userId, 'n' => $name, 'j' => $json, 's' => $size, 'th' => $thumb !== '' ? $thumb : null,
|
||||||
|
'created_at' => $now, 'updated_at' => $now,
|
||||||
|
]);
|
||||||
|
$id = (int)Db::pdo()->lastInsertId();
|
||||||
|
return ['id' => $id, 'name' => $name, 'size' => $size, 'version' => 1, 'updated_at' => $now];
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
self::failDb($e);
|
||||||
}
|
}
|
||||||
$size = strlen($json);
|
|
||||||
$quota = self::quotaBytes($userId);
|
|
||||||
$used = self::usedBytes($userId);
|
|
||||||
if ($quota > 0 && $used + $size > $quota) {
|
|
||||||
Json::fail('quota_exceeded', '存储空间已满,请清理文件或续订', 413);
|
|
||||||
}
|
|
||||||
$now = date('Y-m-d H:i:s');
|
|
||||||
$thumb = TemplateService::thumbFromSoonJson($json);
|
|
||||||
$stmt = Db::pdo()->prepare(
|
|
||||||
'INSERT INTO soon_files (user_id, name, json, size, version, thumb, created_at, updated_at) '
|
|
||||||
. 'VALUES (:u, :n, :j, :s, 1, :th, :created_at, :updated_at)'
|
|
||||||
);
|
|
||||||
$stmt->execute([
|
|
||||||
'u' => $userId, 'n' => $name, 'j' => $json, 's' => $size, 'th' => $thumb !== '' ? $thumb : null,
|
|
||||||
'created_at' => $now, 'updated_at' => $now,
|
|
||||||
]);
|
|
||||||
$id = (int)Db::pdo()->lastInsertId();
|
|
||||||
return ['id' => $id, 'name' => $name, 'size' => $size, 'version' => 1, 'updated_at' => $now];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function update(int $userId, int $id, string $name, string $json, ?int $expectedVersion): array
|
public static function update(int $userId, int $id, string $name, string $json, ?int $expectedVersion): array
|
||||||
{
|
{
|
||||||
|
$name = self::normalizeName($name);
|
||||||
|
self::assertJsonPayload($json);
|
||||||
$newSize = strlen($json);
|
$newSize = strlen($json);
|
||||||
$quota = self::quotaBytes($userId);
|
$quota = self::quotaBytes($userId);
|
||||||
$used = self::usedBytes($userId);
|
$used = self::usedBytes($userId);
|
||||||
@@ -121,7 +133,7 @@ final class FileService
|
|||||||
}
|
}
|
||||||
$now = date('Y-m-d H:i:s');
|
$now = date('Y-m-d H:i:s');
|
||||||
$newVersion = (int)$row['version'] + 1;
|
$newVersion = (int)$row['version'] + 1;
|
||||||
$thumb = TemplateService::thumbFromSoonJson($json);
|
$thumb = self::extractThumb($json);
|
||||||
$upd = $pdo->prepare(
|
$upd = $pdo->prepare(
|
||||||
'UPDATE soon_files SET name = :n, json = :j, size = :s, version = :v, thumb = :th, updated_at = :ts '
|
'UPDATE soon_files SET name = :n, json = :j, size = :s, version = :v, thumb = :th, updated_at = :ts '
|
||||||
. 'WHERE id = :id AND version = :cv'
|
. 'WHERE id = :id AND version = :cv'
|
||||||
@@ -137,17 +149,77 @@ final class FileService
|
|||||||
$pdo->commit();
|
$pdo->commit();
|
||||||
return ['id' => $id, 'name' => $name, 'size' => $newSize, 'version' => $newVersion, 'updated_at' => $now];
|
return ['id' => $id, 'name' => $name, 'size' => $newSize, 'version' => $newVersion, 'updated_at' => $now];
|
||||||
} catch (\RuntimeException $e) {
|
} catch (\RuntimeException $e) {
|
||||||
if ($pdo->inTransaction()) $pdo->rollBack();
|
if ($pdo->inTransaction()) {
|
||||||
if ($e->getMessage() === 'not_found') Json::fail('not_found', '文件不存在', 404);
|
$pdo->rollBack();
|
||||||
if ($e->getMessage() === 'version_conflict') Json::fail('conflict', '版本冲突,请刷新后重试', 409);
|
}
|
||||||
if ($e->getMessage() === 'quota_exceeded') Json::fail('quota_exceeded', '存储空间已满', 413);
|
if ($e->getMessage() === 'not_found') {
|
||||||
|
Json::fail('not_found', '文件不存在', 404);
|
||||||
|
}
|
||||||
|
if ($e->getMessage() === 'version_conflict') {
|
||||||
|
Json::fail('conflict', '版本冲突,请刷新后重试', 409);
|
||||||
|
}
|
||||||
|
if ($e->getMessage() === 'quota_exceeded') {
|
||||||
|
Json::fail('quota_exceeded', '存储空间已满', 413);
|
||||||
|
}
|
||||||
throw $e;
|
throw $e;
|
||||||
} catch (\Throwable $e) {
|
} catch (PDOException $e) {
|
||||||
if ($pdo->inTransaction()) $pdo->rollBack();
|
if ($pdo->inTransaction()) {
|
||||||
|
$pdo->rollBack();
|
||||||
|
}
|
||||||
|
self::failDb($e);
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
if ($pdo->inTransaction()) {
|
||||||
|
$pdo->rollBack();
|
||||||
|
}
|
||||||
throw $e;
|
throw $e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static function normalizeName(string $name): string
|
||||||
|
{
|
||||||
|
$name = trim($name);
|
||||||
|
if ($name === '') {
|
||||||
|
return 'untitled.soon';
|
||||||
|
}
|
||||||
|
if (mb_strlen($name) > 190) {
|
||||||
|
Json::fail('bad_request', '文件名过长', 400);
|
||||||
|
}
|
||||||
|
return $name;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function assertJsonPayload(string $json): void
|
||||||
|
{
|
||||||
|
if ($json === '') {
|
||||||
|
Json::fail('bad_request', '文件内容为空', 400);
|
||||||
|
}
|
||||||
|
$size = strlen($json);
|
||||||
|
if ($size > self::MAX_JSON_BYTES) {
|
||||||
|
Json::fail('payload_too_large', '文件内容过大(最大 32MB)', 413);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function extractThumb(string $json): string
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
return TemplateService::thumbFromSoonJson($json);
|
||||||
|
} catch (Throwable) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return never */
|
||||||
|
private static function failDb(PDOException $e): void
|
||||||
|
{
|
||||||
|
$detail = $e->getMessage();
|
||||||
|
if (str_contains($detail, 'max_allowed_packet') || str_contains($detail, 'Packet too large')) {
|
||||||
|
Json::fail('payload_too_large', '文件过大,超出服务器数据库限制', 413);
|
||||||
|
}
|
||||||
|
if (str_contains($detail, 'Unknown column') || str_contains($detail, "doesn't exist")) {
|
||||||
|
Json::fail('server_error', '数据库结构不完整,请导入 database/schema.sql 或执行 php scripts/migrate-schema.php', 500);
|
||||||
|
}
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
|
||||||
public static function softDelete(int $userId, int $id): void
|
public static function softDelete(int $userId, int $id): void
|
||||||
{
|
{
|
||||||
$stmt = Db::pdo()->prepare('UPDATE soon_files SET deleted_at = :ts WHERE id = :id AND user_id = :u');
|
$stmt = Db::pdo()->prepare('UPDATE soon_files SET deleted_at = :ts WHERE id = :id AND user_id = :u');
|
||||||
|
|||||||
@@ -96,3 +96,5 @@ require SOON_API_ROOT . '/Services/PaymentConfigLoader.php';
|
|||||||
require SOON_API_ROOT . '/Services/SettingsConfigLoader.php';
|
require SOON_API_ROOT . '/Services/SettingsConfigLoader.php';
|
||||||
\Soon\Api\Services\PaymentConfigLoader::reload($config);
|
\Soon\Api\Services\PaymentConfigLoader::reload($config);
|
||||||
\Soon\Api\Core\Config::init($config);
|
\Soon\Api\Core\Config::init($config);
|
||||||
|
require SOON_API_ROOT . '/Core/ErrorHandler.php';
|
||||||
|
\Soon\Api\Core\ErrorHandler::register();
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ services:
|
|||||||
- --collation-server=utf8mb4_unicode_ci
|
- --collation-server=utf8mb4_unicode_ci
|
||||||
volumes:
|
volumes:
|
||||||
- soondesign_mysql_data:/var/lib/mysql
|
- soondesign_mysql_data:/var/lib/mysql
|
||||||
- ../backend-web/schema.sql:/docker-entrypoint-initdb.d/01-schema.sql:ro
|
- ../backend-web/database/schema.sql:/docker-entrypoint-initdb.d/01-schema.sql:ro
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-uroot", "-p${MYSQL_ROOT_PASSWORD:-rootlocal}"]
|
test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-uroot", "-p${MYSQL_ROOT_PASSWORD:-rootlocal}"]
|
||||||
interval: 5s
|
interval: 5s
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
<?php
|
<?php
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
require dirname(__DIR__, 2) . '/backend-web/scripts/schema-migrate-lib.php';
|
||||||
|
|
||||||
$host = getenv('SOON_DB_HOST') ?: 'mysql';
|
$host = getenv('SOON_DB_HOST') ?: 'mysql';
|
||||||
$port = (int)(getenv('SOON_DB_PORT') ?: 3306);
|
$port = (int)(getenv('SOON_DB_PORT') ?: 3306);
|
||||||
$user = getenv('SOON_DB_USER') ?: 'soondesign';
|
$user = getenv('SOON_DB_USER') ?: 'soondesign';
|
||||||
@@ -14,151 +16,8 @@ try {
|
|||||||
$pass,
|
$pass,
|
||||||
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
|
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
|
||||||
);
|
);
|
||||||
|
soon_schema_migrate($pdo, 'migrate-dev-schema');
|
||||||
} catch (Throwable $e) {
|
} catch (Throwable $e) {
|
||||||
fwrite(STDERR, 'migrate-dev-schema: ' . $e->getMessage() . PHP_EOL);
|
fwrite(STDERR, 'migrate-dev-schema: ' . $e->getMessage() . PHP_EOL);
|
||||||
exit(1);
|
exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
function columnExists(PDO $pdo, string $table, string $column): bool
|
|
||||||
{
|
|
||||||
$stmt = $pdo->prepare(
|
|
||||||
'SELECT COUNT(*) FROM information_schema.COLUMNS '
|
|
||||||
. 'WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = :t AND COLUMN_NAME = :c'
|
|
||||||
);
|
|
||||||
$stmt->execute(['t' => $table, 'c' => $column]);
|
|
||||||
return (int)$stmt->fetchColumn() > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
$alters = [];
|
|
||||||
if (!columnExists($pdo, 'subscriptions', 'source_order_id')) {
|
|
||||||
$alters[] = 'ALTER TABLE subscriptions ADD COLUMN source_order_id BIGINT UNSIGNED DEFAULT NULL AFTER plan_id';
|
|
||||||
$alters[] = 'ALTER TABLE subscriptions ADD KEY source_order (source_order_id)';
|
|
||||||
}
|
|
||||||
if (!columnExists($pdo, 'pay_orders', 'refund_status')) {
|
|
||||||
$alters[] = "ALTER TABLE pay_orders ADD COLUMN refund_status ENUM('none','pending','approved','rejected') NOT NULL DEFAULT 'none' AFTER status";
|
|
||||||
$alters[] = 'ALTER TABLE pay_orders ADD COLUMN refund_reason TEXT DEFAULT NULL AFTER refund_status';
|
|
||||||
$alters[] = 'ALTER TABLE pay_orders ADD COLUMN refund_note TEXT DEFAULT NULL AFTER refund_reason';
|
|
||||||
$alters[] = 'ALTER TABLE pay_orders ADD COLUMN cancelled_at DATETIME DEFAULT NULL AFTER paid_at';
|
|
||||||
$alters[] = 'ALTER TABLE pay_orders ADD COLUMN refunded_at DATETIME DEFAULT NULL AFTER cancelled_at';
|
|
||||||
$alters[] = 'ALTER TABLE pay_orders ADD KEY refund_status (refund_status)';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!columnExists($pdo, 'plans', 'description')) {
|
|
||||||
$alters[] = 'ALTER TABLE plans ADD COLUMN description VARCHAR(255) DEFAULT NULL AFTER name';
|
|
||||||
$alters[] = 'ALTER TABLE plans ADD COLUMN max_files INT UNSIGNED NOT NULL DEFAULT 0 AFTER quota_mb';
|
|
||||||
$alters[] = 'ALTER TABLE plans ADD COLUMN features TEXT DEFAULT NULL AFTER duration_days';
|
|
||||||
$alters[] = 'ALTER TABLE plans ADD COLUMN sort_order INT NOT NULL DEFAULT 0 AFTER features';
|
|
||||||
$alters[] = 'ALTER TABLE plans ADD COLUMN is_recommended TINYINT(1) NOT NULL DEFAULT 0 AFTER sort_order';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!columnExists($pdo, 'users', 'admin_level')) {
|
|
||||||
$alters[] = "ALTER TABLE users ADD COLUMN admin_level ENUM('full','ops') DEFAULT NULL AFTER role";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!columnExists($pdo, 'soon_files', 'thumb')) {
|
|
||||||
$alters[] = 'ALTER TABLE soon_files ADD COLUMN thumb MEDIUMTEXT DEFAULT NULL AFTER version';
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ($alters as $sql) {
|
|
||||||
$pdo->exec($sql);
|
|
||||||
fwrite(STDOUT, "migrate-dev-schema: {$sql}" . PHP_EOL);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (columnExists($pdo, 'users', 'admin_level')) {
|
|
||||||
$pdo->exec(
|
|
||||||
"UPDATE users SET admin_level = 'full' WHERE role = 'admin' AND (admin_level IS NULL OR admin_level = '')"
|
|
||||||
);
|
|
||||||
fwrite(STDOUT, 'migrate-dev-schema: admin_level backfill for existing admins' . PHP_EOL);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (columnExists($pdo, 'plans', 'description')) {
|
|
||||||
$legacyCodes = ['pro_monthly' => 'member_monthly', 'pro_yearly' => 'member_yearly'];
|
|
||||||
foreach ($legacyCodes as $oldCode => $newCode) {
|
|
||||||
$stmt = $pdo->prepare('SELECT id FROM plans WHERE code = :c LIMIT 1');
|
|
||||||
$stmt->execute(['c' => $oldCode]);
|
|
||||||
if ($stmt->fetch()) {
|
|
||||||
$conflict = $pdo->prepare('SELECT id FROM plans WHERE code = :c LIMIT 1');
|
|
||||||
$conflict->execute(['c' => $newCode]);
|
|
||||||
if ($conflict->fetch()) {
|
|
||||||
$pdo->prepare('UPDATE plans SET is_active = 0 WHERE code = :c')->execute(['c' => $oldCode]);
|
|
||||||
} else {
|
|
||||||
$pdo->prepare('UPDATE plans SET code = :new WHERE code = :old')->execute(['new' => $newCode, 'old' => $oldCode]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$quota = 2048;
|
|
||||||
$maxFiles = 200;
|
|
||||||
$seed = [
|
|
||||||
['free', '免费版', '免费体验设计与编辑', 0, $quota, $maxFiles, 0,
|
|
||||||
'["设计与编辑工具免费使用"]', 0, 0, 1],
|
|
||||||
['member_lifetime', '永久会员', '一次激活,永久使用预览交付能力', 100, $quota, $maxFiles, 0,
|
|
||||||
'["高清预览","成品打印","云端保存","导出设计文件","永久有效"]', 10, 1, 1],
|
|
||||||
['member_monthly', '月度订阅', '按月灵活使用,随时续订', 1999, $quota, $maxFiles, 30,
|
|
||||||
'["高清预览","成品打印","云端保存","导出设计文件","订阅周期:1 个月"]', 20, 0, 0],
|
|
||||||
['member_quarterly', '季度订阅', '连续三个月,更省心', 5299, $quota, $maxFiles, 90,
|
|
||||||
'["高清预览","成品打印","云端保存","导出设计文件","订阅周期:1 季"]', 25, 0, 0],
|
|
||||||
['member_yearly', '年度订阅', '全年畅享,性价比更高', 19999, $quota, $maxFiles, 365,
|
|
||||||
'["高清预览","成品打印","云端保存","导出设计文件","订阅周期:1 年"]', 30, 0, 0],
|
|
||||||
];
|
|
||||||
$upsert = $pdo->prepare(
|
|
||||||
'INSERT INTO plans (code, name, description, price_cents, quota_mb, max_files, duration_days, features, sort_order, is_recommended, is_active) '
|
|
||||||
. 'VALUES (:c, :n, :d, :p, :q, :mf, :dd, :f, :so, :ir, :a) '
|
|
||||||
. 'ON DUPLICATE KEY UPDATE name=VALUES(name), description=VALUES(description), price_cents=VALUES(price_cents), '
|
|
||||||
. 'quota_mb=VALUES(quota_mb), max_files=VALUES(max_files), duration_days=VALUES(duration_days), '
|
|
||||||
. 'features=VALUES(features), sort_order=VALUES(sort_order), is_recommended=VALUES(is_recommended), is_active=VALUES(is_active)'
|
|
||||||
);
|
|
||||||
foreach ($seed as $row) {
|
|
||||||
$upsert->execute([
|
|
||||||
'c' => $row[0], 'n' => $row[1], 'd' => $row[2], 'p' => $row[3], 'q' => $row[4],
|
|
||||||
'mf' => $row[5], 'dd' => $row[6], 'f' => $row[7], 'so' => $row[8], 'ir' => $row[9], 'a' => $row[10],
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
fwrite(STDOUT, 'migrate-dev-schema: membership plans catalog synced' . PHP_EOL);
|
|
||||||
|
|
||||||
$legacyFeatStmt = $pdo->query(
|
|
||||||
'SELECT code, duration_days FROM plans WHERE features LIKE \'%预览成品效果%\' '
|
|
||||||
. 'OR features LIKE \'%到期后恢复为非会员%\' OR features LIKE \'%会员权益一致%\' '
|
|
||||||
. 'OR name LIKE \'%会员 ·%\''
|
|
||||||
);
|
|
||||||
$featUpd = $pdo->prepare('UPDATE plans SET features = :f WHERE code = :c');
|
|
||||||
$coreFeatures = ['高清预览', '成品打印', '云端保存', '导出设计文件'];
|
|
||||||
foreach ($legacyFeatStmt->fetchAll() as $legacyRow) {
|
|
||||||
$code = (string)$legacyRow['code'];
|
|
||||||
$days = (int)$legacyRow['duration_days'];
|
|
||||||
if ($code === 'free') {
|
|
||||||
$featUpd->execute([
|
|
||||||
'f' => '["设计与编辑工具免费使用"]',
|
|
||||||
'c' => $code,
|
|
||||||
]);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$lines = $coreFeatures;
|
|
||||||
if ($days > 0) {
|
|
||||||
$period = $days >= 365 ? '1 年' : ($days === 90 ? '1 季' : ($days >= 30 && $days % 30 === 0 ? ((int)($days / 30)) . ' 个月' : $days . ' 天'));
|
|
||||||
$lines[] = '订阅周期:' . $period;
|
|
||||||
}
|
|
||||||
$featUpd->execute([
|
|
||||||
'f' => json_encode($lines, JSON_UNESCAPED_UNICODE),
|
|
||||||
'c' => $code,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
fwrite(STDOUT, 'migrate-dev-schema: legacy plan copy refreshed' . PHP_EOL);
|
|
||||||
}
|
|
||||||
|
|
||||||
$pdo->exec(
|
|
||||||
'CREATE TABLE IF NOT EXISTS `soon_templates` ('
|
|
||||||
. '`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,'
|
|
||||||
. '`name` VARCHAR(120) NOT NULL,'
|
|
||||||
. '`type` TINYINT UNSIGNED NOT NULL DEFAULT 1,'
|
|
||||||
. '`thumb` MEDIUMTEXT DEFAULT NULL,'
|
|
||||||
. '`file_size` INT UNSIGNED NOT NULL DEFAULT 0,'
|
|
||||||
. '`sort_order` INT NOT NULL DEFAULT 0,'
|
|
||||||
. '`is_active` TINYINT(1) NOT NULL DEFAULT 1,'
|
|
||||||
. '`created_at` DATETIME NOT NULL,'
|
|
||||||
. '`updated_at` DATETIME NOT NULL,'
|
|
||||||
. 'PRIMARY KEY (`id`),'
|
|
||||||
. 'KEY `active_sort` (`is_active`, `sort_order`, `id`)'
|
|
||||||
. ') ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci'
|
|
||||||
);
|
|
||||||
fwrite(STDOUT, 'migrate-dev-schema: soon_templates ensured' . PHP_EOL);
|
|
||||||
|
|||||||
@@ -250,6 +250,17 @@ textarea:hover{
|
|||||||
border: #646E76 solid 1px;
|
border: #646E76 solid 1px;
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
|
min-width: 160px;
|
||||||
|
max-width: 100%;
|
||||||
|
height: 32px;
|
||||||
|
line-height: 32px;
|
||||||
|
padding: 0 28px 0 8px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.component-line .ui-select{
|
||||||
|
width: calc(100% - 72px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -215,6 +215,15 @@ body.soon-design-page .layui-layer.soon-layer--login-gate .layui-layer-setwin a.
|
|||||||
background: transparent !important;
|
background: transparent !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.layui-layer.soon-layer .layui-layer-content.soon-layer-content--form,
|
||||||
|
.layui-layer-soon-layer .layui-layer-content.soon-layer-content--form {
|
||||||
|
display: flex !important;
|
||||||
|
flex-direction: column !important;
|
||||||
|
overflow: hidden !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
.layui-layer.soon-layer .layui-layer-content.soon-layer-content--scroll,
|
.layui-layer.soon-layer .layui-layer-content.soon-layer-content--scroll,
|
||||||
.layui-layer-soon-layer .layui-layer-content.soon-layer-content--scroll {
|
.layui-layer-soon-layer .layui-layer-content.soon-layer-content--scroll {
|
||||||
overflow-x: hidden !important;
|
overflow-x: hidden !important;
|
||||||
|
|||||||
@@ -6,3 +6,4 @@ window.SOON_DEPLOY_CONFIG = {
|
|||||||
web_base: 'https://design.cardsoon.com',
|
web_base: 'https://design.cardsoon.com',
|
||||||
version: '1.0.0'
|
version: '1.0.0'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -85,16 +85,21 @@
|
|||||||
fullUrl = getApiBase() + '/' + String(url).replace(/^\/+/, '');
|
fullUrl = getApiBase() + '/' + String(url).replace(/^\/+/, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var hadSession = !!(token || getRefreshToken());
|
||||||
|
|
||||||
return fetch(fullUrl, opts).then(function (response) {
|
return fetch(fullUrl, opts).then(function (response) {
|
||||||
if (response.status !== 401) return response;
|
if (response.status !== 401) return response;
|
||||||
|
if (!getRefreshToken()) return response;
|
||||||
return refreshAccessToken().then(function (newToken) {
|
return refreshAccessToken().then(function (newToken) {
|
||||||
var retryHeaders = Object.assign({}, opts.headers || {});
|
var retryHeaders = Object.assign({}, opts.headers || {});
|
||||||
retryHeaders['Authorization'] = 'Bearer ' + newToken;
|
retryHeaders['Authorization'] = 'Bearer ' + newToken;
|
||||||
var retryOpts = Object.assign({}, opts, { headers: retryHeaders });
|
var retryOpts = Object.assign({}, opts, { headers: retryHeaders });
|
||||||
return fetch(fullUrl, retryOpts);
|
return fetch(fullUrl, retryOpts);
|
||||||
}).catch(function () {
|
}).catch(function () {
|
||||||
clearSession();
|
if (hadSession) {
|
||||||
redirectToLogin();
|
clearSession();
|
||||||
|
redirectToLogin();
|
||||||
|
}
|
||||||
return response;
|
return response;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -88,22 +88,29 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (hidden.length) canvas.renderAll();
|
if (hidden.length) canvas.renderAll();
|
||||||
|
window._soonSkipDesign1CdMask = true;
|
||||||
var mult = 0.18;
|
var mult = 0.18;
|
||||||
var quality = 0.72;
|
var quality = 0.72;
|
||||||
var url = '';
|
var url = '';
|
||||||
for (var i = 0; i < 3; i++) {
|
try {
|
||||||
try {
|
for (var i = 0; i < 3; i++) {
|
||||||
var candidate = canvas.toDataURL({ format: 'jpeg', quality: quality, multiplier: mult });
|
try {
|
||||||
if (candidate && candidate.indexOf('data:image/') === 0 && candidate.length <= THUMB_MAX) {
|
var candidate = canvas.toDataURL({ format: 'jpeg', quality: quality, multiplier: mult });
|
||||||
url = candidate;
|
if (candidate && candidate.indexOf('data:image/') === 0 && candidate.length <= THUMB_MAX) {
|
||||||
break;
|
url = candidate;
|
||||||
}
|
break;
|
||||||
} catch (e) { /* ignore */ }
|
}
|
||||||
quality -= 0.18;
|
} catch (e) { /* ignore */ }
|
||||||
mult -= 0.04;
|
quality -= 0.18;
|
||||||
|
mult -= 0.04;
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
window._soonSkipDesign1CdMask = false;
|
||||||
}
|
}
|
||||||
hidden.forEach(function (obj) { obj.visible = true; });
|
hidden.forEach(function (obj) { obj.visible = true; });
|
||||||
if (hidden.length) canvas.renderAll();
|
canvas.renderAll();
|
||||||
|
if (typeof window.soonClearFabricOverlay === 'function') window.soonClearFabricOverlay(canvas);
|
||||||
|
if (canvas.requestRenderAll) canvas.requestRenderAll();
|
||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,8 +141,20 @@
|
|||||||
return soonEnsureSoonExt(name);
|
return soonEnsureSoonExt(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function dismissLayerMask() {
|
||||||
|
if (typeof window.soonCleanupLayerMask === 'function') {
|
||||||
|
window.soonCleanupLayerMask();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
soonHideOpenFileLoading();
|
||||||
|
invokeLayerCleanup();
|
||||||
|
}
|
||||||
|
|
||||||
function soonReportSaveError(err) {
|
function soonReportSaveError(err) {
|
||||||
|
if (typeof window.soonFinishSaveUi === 'function') window.soonFinishSaveUi();
|
||||||
|
else dismissLayerMask();
|
||||||
if (err && err.conflictSynced) return;
|
if (err && err.conflictSynced) return;
|
||||||
|
if (err && err.apiNotified) return;
|
||||||
if (typeof window.soonShowApiError === 'function') {
|
if (typeof window.soonShowApiError === 'function') {
|
||||||
window.soonShowApiError({
|
window.soonShowApiError({
|
||||||
status: err && err.status,
|
status: err && err.status,
|
||||||
@@ -149,11 +168,27 @@
|
|||||||
|
|
||||||
function soonWriteSoonContentWeb(fp, content, prevOpenKey, onDone, onError) {
|
function soonWriteSoonContentWeb(fp, content, prevOpenKey, onDone, onError) {
|
||||||
if (!window.platformBridge || !window.platformBridge.writeFile) return false;
|
if (!window.platformBridge || !window.platformBridge.writeFile) return false;
|
||||||
window.platformBridge.writeFile(fp, content).then(function (res) {
|
if (typeof window.soonShowOpenFileLoading === 'function') window.soonShowOpenFileLoading();
|
||||||
|
var writePromise;
|
||||||
|
try {
|
||||||
|
writePromise = window.platformBridge.writeFile(fp, content);
|
||||||
|
} catch (syncErr) {
|
||||||
|
dismissLayerMask();
|
||||||
|
if (typeof onError === 'function') onError(syncErr);
|
||||||
|
else soonReportSaveError(syncErr);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!writePromise || typeof writePromise.then !== 'function') {
|
||||||
|
dismissLayerMask();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
writePromise.then(function (res) {
|
||||||
if (typeof onDone === 'function') onDone(res, prevOpenKey);
|
if (typeof onDone === 'function') onDone(res, prevOpenKey);
|
||||||
}).catch(function (err) {
|
}).catch(function (err) {
|
||||||
if (typeof onError === 'function') onError(err);
|
if (typeof onError === 'function') onError(err);
|
||||||
else soonReportSaveError(err);
|
else soonReportSaveError(err);
|
||||||
|
}).finally(function () {
|
||||||
|
dismissLayerMask();
|
||||||
});
|
});
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -656,37 +691,52 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function invokeLayerCleanup() {
|
||||||
|
if (typeof window.soonCleanupLayerMask === 'function') {
|
||||||
|
window.soonCleanupLayerMask();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function soonReportOpenLoadError(message) {
|
function soonReportOpenLoadError(message) {
|
||||||
soonHideOpenFileLoading();
|
soonHideOpenFileLoading();
|
||||||
|
invokeLayerCleanup();
|
||||||
soonToast(message || '文件加载失败', 'error');
|
soonToast(message || '文件加载失败', 'error');
|
||||||
}
|
}
|
||||||
|
|
||||||
function soonShowOpenFileLoading() {
|
function soonShowOpenFileLoading() {
|
||||||
if (typeof layer === 'undefined' || !layer.load) return;
|
if (typeof layer === 'undefined' || !layer.load) return;
|
||||||
soonHideOpenFileLoading();
|
soonHideOpenFileLoading();
|
||||||
window._soonOpenLoadIdx = layer.load(1, { shade: [0.12, '#000'] });
|
window._soonOpenLoadIdx = layer.load(1, { shade: 0 });
|
||||||
}
|
}
|
||||||
|
|
||||||
function soonHideOpenFileLoading() {
|
function soonHideOpenFileLoading() {
|
||||||
if (window._soonOpenLoadIdx == null) return;
|
if (typeof layer !== 'undefined' && layer.closeAll) {
|
||||||
if (typeof layer !== 'undefined' && layer.close) layer.close(window._soonOpenLoadIdx);
|
layer.closeAll('loading');
|
||||||
|
}
|
||||||
|
if (window._soonOpenLoadIdx != null && typeof layer !== 'undefined' && layer.close) {
|
||||||
|
try { layer.close(window._soonOpenLoadIdx); } catch (e) { /* ignore */ }
|
||||||
|
}
|
||||||
window._soonOpenLoadIdx = null;
|
window._soonOpenLoadIdx = null;
|
||||||
|
invokeLayerCleanup();
|
||||||
}
|
}
|
||||||
|
|
||||||
function soonToast(message, type) {
|
function soonToast(message, type) {
|
||||||
if (typeof window.soonToast === 'function') window.soonToast(message, type);
|
if (typeof window.soonToast === 'function') window.soonToast(message, type);
|
||||||
}
|
}
|
||||||
|
|
||||||
function soonRequireLogin(actionLabel) {
|
function soonRequireLogin(actionLabel, options) {
|
||||||
|
options = options || {};
|
||||||
if (soonGetAccessToken()) return true;
|
if (soonGetAccessToken()) return true;
|
||||||
var label = actionLabel || '继续';
|
var label = actionLabel || '继续';
|
||||||
soonToast('请先登录后再' + label, 'warn');
|
soonToast('请先登录后再' + (label || '操作'), 'warn');
|
||||||
if (typeof window.soonRedirectToLogin === 'function') {
|
if (options.redirect) {
|
||||||
setTimeout(function () { window.soonRedirectToLogin(); }, 600);
|
if (typeof window.soonRedirectToLogin === 'function') {
|
||||||
} else if (typeof window.redirectToLogin === 'function') {
|
setTimeout(function () { window.soonRedirectToLogin(); }, 600);
|
||||||
setTimeout(function () { window.redirectToLogin(); }, 600);
|
} else if (typeof window.redirectToLogin === 'function') {
|
||||||
} else {
|
setTimeout(function () { window.redirectToLogin(); }, 600);
|
||||||
window.location.href = 'login.web.html';
|
} else {
|
||||||
|
window.location.href = 'login.web.html';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -724,8 +774,8 @@
|
|||||||
else if (info.status === 409) kind = 'warn';
|
else if (info.status === 409) kind = 'warn';
|
||||||
else if (info.status === 413) kind = 'warn';
|
else if (info.status === 413) kind = 'warn';
|
||||||
soonToast(info.message, kind);
|
soonToast(info.message, kind);
|
||||||
if (info.status === 401) {
|
if (info.status === 401 && info.requireLogin) {
|
||||||
soonRequireLogin('操作');
|
soonRequireLogin('操作', { redirect: true });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -79,6 +79,7 @@
|
|||||||
if (idx != null) layer.close(idx);
|
if (idx != null) layer.close(idx);
|
||||||
if (payIdx != null) layer.close(payIdx);
|
if (payIdx != null) layer.close(payIdx);
|
||||||
}
|
}
|
||||||
|
if (typeof window.soonCleanupLayerMask === 'function') window.soonCleanupLayerMask();
|
||||||
if (fireStay && opts && typeof opts.onStay === 'function') opts.onStay();
|
if (fireStay && opts && typeof opts.onStay === 'function') opts.onStay();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,6 +94,7 @@
|
|||||||
_gate.payIndex = null;
|
_gate.payIndex = null;
|
||||||
if (typeof layer !== 'undefined' && gateIdx != null) layer.close(gateIdx);
|
if (typeof layer !== 'undefined' && gateIdx != null) layer.close(gateIdx);
|
||||||
_gate = { index: null, payIndex: null, plan: null, gateOpts: null };
|
_gate = { index: null, payIndex: null, plan: null, gateOpts: null };
|
||||||
|
if (typeof window.soonCleanupLayerMask === 'function') window.soonCleanupLayerMask();
|
||||||
toast('恭喜您,会员已激活!', 'success');
|
toast('恭喜您,会员已激活!', 'success');
|
||||||
if (typeof window.soonLoadMembership === 'function') {
|
if (typeof window.soonLoadMembership === 'function') {
|
||||||
window.soonLoadMembership(true).then(function () {
|
window.soonLoadMembership(true).then(function () {
|
||||||
|
|||||||
@@ -69,6 +69,7 @@
|
|||||||
if (idx != null && typeof layer !== 'undefined') {
|
if (idx != null && typeof layer !== 'undefined') {
|
||||||
try { layer.close(idx); } catch (e) { /* ignore */ }
|
try { layer.close(idx); } catch (e) { /* ignore */ }
|
||||||
}
|
}
|
||||||
|
if (typeof window.soonCleanupLayerMask === 'function') window.soonCleanupLayerMask();
|
||||||
}
|
}
|
||||||
|
|
||||||
function bindModal(modal) {
|
function bindModal(modal) {
|
||||||
|
|||||||
@@ -607,6 +607,7 @@
|
|||||||
displayChannels: channels,
|
displayChannels: channels,
|
||||||
onPaid: function () {
|
onPaid: function () {
|
||||||
layer.close(idx);
|
layer.close(idx);
|
||||||
|
if (typeof window.soonCleanupLayerMask === 'function') window.soonCleanupLayerMask();
|
||||||
if (typeof opts.onPaid === 'function') opts.onPaid();
|
if (typeof opts.onPaid === 'function') opts.onPaid();
|
||||||
},
|
},
|
||||||
onBack: opts.onBack,
|
onBack: opts.onBack,
|
||||||
@@ -617,6 +618,7 @@
|
|||||||
},
|
},
|
||||||
end: function () {
|
end: function () {
|
||||||
if (payCtrl) payCtrl.destroy();
|
if (payCtrl) payCtrl.destroy();
|
||||||
|
if (typeof window.soonCleanupLayerMask === 'function') window.soonCleanupLayerMask();
|
||||||
if (typeof opts.onClose === 'function') opts.onClose();
|
if (typeof opts.onClose === 'function') opts.onClose();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -17,6 +17,9 @@
|
|||||||
localStorage.removeItem('soon_access');
|
localStorage.removeItem('soon_access');
|
||||||
localStorage.removeItem('soon_refresh');
|
localStorage.removeItem('soon_refresh');
|
||||||
}
|
}
|
||||||
|
if (typeof window.soonApplyMembership === 'function') {
|
||||||
|
window.soonApplyMembership(null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function initClearCache() {
|
function initClearCache() {
|
||||||
|
|||||||
@@ -1,14 +1,81 @@
|
|||||||
(function () {
|
(function () {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
function soonCleanupLayerMask() {
|
||||||
|
if (typeof layer !== 'undefined' && layer.closeAll) {
|
||||||
|
layer.closeAll('loading');
|
||||||
|
}
|
||||||
|
if (window._soonOpenLoadIdx != null && typeof layer !== 'undefined' && layer.close) {
|
||||||
|
try { layer.close(window._soonOpenLoadIdx); } catch (e) { /* ignore */ }
|
||||||
|
window._soonOpenLoadIdx = null;
|
||||||
|
}
|
||||||
|
if (typeof document === 'undefined') return;
|
||||||
|
var layers = document.querySelectorAll('.layui-layer');
|
||||||
|
for (var j = 0; j < layers.length; j++) {
|
||||||
|
if (layers[j].getAttribute('type') !== 'loading') continue;
|
||||||
|
var loadTimes = layers[j].getAttribute('times');
|
||||||
|
if (loadTimes && typeof layer !== 'undefined' && layer.close) {
|
||||||
|
try { layer.close(loadTimes); } catch (e2) { /* ignore */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var shades = document.querySelectorAll('.layui-layer-shade');
|
||||||
|
for (var i = shades.length - 1; i >= 0; i--) {
|
||||||
|
var shade = shades[i];
|
||||||
|
var times = shade.getAttribute('times');
|
||||||
|
var layerEl = times ? document.getElementById('layui-layer' + times) : null;
|
||||||
|
if (!layerEl || layerEl.getAttribute('type') === 'loading') {
|
||||||
|
if (shade.parentNode) shade.parentNode.removeChild(shade);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function soonLayerDismiss(type) {
|
||||||
|
if (typeof layer !== 'undefined' && layer.closeAll) {
|
||||||
|
layer.closeAll(type);
|
||||||
|
}
|
||||||
|
soonCleanupLayerMask();
|
||||||
|
}
|
||||||
|
|
||||||
|
function soonClearFabricOverlay(canvas) {
|
||||||
|
if (!canvas || !canvas.contextTop) return;
|
||||||
|
if (typeof canvas.clearContext === 'function') {
|
||||||
|
canvas.clearContext(canvas.contextTop);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function soonRefreshDesignCanvases() {
|
||||||
|
var names = ['canvas1', 'canvas2', 'canvas'];
|
||||||
|
for (var n = 0; n < names.length; n++) {
|
||||||
|
var c = window[names[n]];
|
||||||
|
if (!c || typeof c.renderAll !== 'function') continue;
|
||||||
|
if (c.discardActiveObject) c.discardActiveObject();
|
||||||
|
soonClearFabricOverlay(c);
|
||||||
|
if (c.requestRenderAll) c.requestRenderAll();
|
||||||
|
else c.renderAll();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function soonFinishSaveUi() {
|
||||||
|
if (typeof window.soonHideOpenFileLoading === 'function') window.soonHideOpenFileLoading();
|
||||||
|
else soonCleanupLayerMask();
|
||||||
|
soonRefreshDesignCanvases();
|
||||||
|
if (typeof requestAnimationFrame === 'function') {
|
||||||
|
requestAnimationFrame(function () {
|
||||||
|
soonCleanupLayerMask();
|
||||||
|
soonRefreshDesignCanvases();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function soonToast(message, type) {
|
function soonToast(message, type) {
|
||||||
if (typeof layer === 'undefined' || !layer.msg) return;
|
if (typeof layer === 'undefined' || !layer.msg) return;
|
||||||
var skin = 'soon-toast';
|
var skin = 'soon-toast';
|
||||||
if (type === 'success') skin += ' soon-toast--success';
|
if (type === 'success') skin += ' soon-toast--success';
|
||||||
else if (type === 'error') skin += ' soon-toast--error';
|
else if (type === 'error') skin += ' soon-toast--error';
|
||||||
else if (type === 'warn') skin += ' soon-toast--warn';
|
else if (type === 'warn') skin += ' soon-toast--warn';
|
||||||
|
var time = type === 'success' ? 2500 : (type === 'error' ? 4200 : 3600);
|
||||||
layer.msg(message, {
|
layer.msg(message, {
|
||||||
time: 2400,
|
time: time,
|
||||||
shade: 0,
|
shade: 0,
|
||||||
offset: '72px',
|
offset: '72px',
|
||||||
icon: -1,
|
icon: -1,
|
||||||
@@ -16,5 +83,10 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
window.soonCleanupLayerMask = soonCleanupLayerMask;
|
||||||
|
window.soonLayerDismiss = soonLayerDismiss;
|
||||||
|
window.soonClearFabricOverlay = soonClearFabricOverlay;
|
||||||
|
window.soonRefreshDesignCanvases = soonRefreshDesignCanvases;
|
||||||
|
window.soonFinishSaveUi = soonFinishSaveUi;
|
||||||
window.soonToast = soonToast;
|
window.soonToast = soonToast;
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -102,10 +102,10 @@ function applyFontList(data) {
|
|||||||
$("#count_font_family").append("<option value='" + item + "'>" + item + "</option>");
|
$("#count_font_family").append("<option value='" + item + "'>" + item + "</option>");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (typeof ipcRenderer !== 'undefined' && ipcRenderer) {
|
if (typeof window !== 'undefined' && window.platformBridge && window.platformBridge.getSystemFonts) {
|
||||||
ipcRenderer.on('font-list', function(event, data) { applyFontList(data); });
|
|
||||||
} else if (typeof window !== 'undefined' && window.platformBridge && window.platformBridge.getSystemFonts) {
|
|
||||||
window.platformBridge.getSystemFonts().then(applyFontList);
|
window.platformBridge.getSystemFonts().then(applyFontList);
|
||||||
|
} else if (typeof ipcRenderer !== 'undefined' && ipcRenderer) {
|
||||||
|
ipcRenderer.on('font-list', function(event, data) { applyFontList(data); });
|
||||||
}
|
}
|
||||||
function applySysLan(data) {
|
function applySysLan(data) {
|
||||||
let lang = localStorage.getItem("lang");
|
let lang = localStorage.getItem("lang");
|
||||||
@@ -1816,7 +1816,7 @@ function initCanvasEvents() {
|
|||||||
const ctx = currentCanvas.getContext();
|
const ctx = currentCanvas.getContext();
|
||||||
let bgObj = (currentCanvas === canvas1) ? background_image1 : background_image2;
|
let bgObj = (currentCanvas === canvas1) ? background_image1 : background_image2;
|
||||||
|
|
||||||
if (bgObj) {
|
if (bgObj && !window._soonSkipDesign1CdMask) {
|
||||||
const width = currentCanvas.width;
|
const width = currentCanvas.width;
|
||||||
const height = currentCanvas.height;
|
const height = currentCanvas.height;
|
||||||
|
|
||||||
|
|||||||
@@ -1204,6 +1204,9 @@ window.openFile = function open(file, jAlready) {
|
|||||||
}).catch(function () {
|
}).catch(function () {
|
||||||
if (typeof window.soonReportOpenLoadError === 'function') window.soonReportOpenLoadError('模板/文件加载失败');
|
if (typeof window.soonReportOpenLoadError === 'function') window.soonReportOpenLoadError('模板/文件加载失败');
|
||||||
else if (typeof window.soonHideOpenFileLoading === 'function') window.soonHideOpenFileLoading();
|
else if (typeof window.soonHideOpenFileLoading === 'function') window.soonHideOpenFileLoading();
|
||||||
|
}).finally(function () {
|
||||||
|
if (typeof window.soonHideOpenFileLoading === 'function') window.soonHideOpenFileLoading();
|
||||||
|
if (typeof window.soonCleanupLayerMask === 'function') window.soonCleanupLayerMask();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@@ -1277,6 +1280,8 @@ function soonPageDesignType() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function onCloudWriteDone(res, fallback, callback) {
|
function onCloudWriteDone(res, fallback, callback) {
|
||||||
|
if (typeof window.soonFinishSaveUi === 'function') window.soonFinishSaveUi();
|
||||||
|
else if (typeof window.soonCleanupLayerMask === 'function') window.soonCleanupLayerMask();
|
||||||
applyCloudWriteResult(res, fallback);
|
applyCloudWriteResult(res, fallback);
|
||||||
if (res && res.fileKey && typeof window.soonClearPendingImport === 'function') {
|
if (res && res.fileKey && typeof window.soonClearPendingImport === 'function') {
|
||||||
window.soonClearPendingImport();
|
window.soonClearPendingImport();
|
||||||
@@ -1284,7 +1289,12 @@ function onCloudWriteDone(res, fallback, callback) {
|
|||||||
if (typeof window.soonRecentOnCloudSave === 'function') {
|
if (typeof window.soonRecentOnCloudSave === 'function') {
|
||||||
window.soonRecentOnCloudSave(res, fallback, soonPageDesignType());
|
window.soonRecentOnCloudSave(res, fallback, soonPageDesignType());
|
||||||
}
|
}
|
||||||
layer.msg(language_str("saveSucc") + cloudSaveSuccessLabel(res, fallback));
|
var msg = language_str("saveSucc") + cloudSaveSuccessLabel(res, fallback);
|
||||||
|
if (res && res.syncFailed) {
|
||||||
|
msg = '已保存到本地,云端同步失败';
|
||||||
|
}
|
||||||
|
if (typeof window.soonToast === 'function') window.soonToast(msg, res && res.syncFailed ? 'warn' : 'success');
|
||||||
|
else layer.msg(msg, { shade: 0, time: 2500 });
|
||||||
if (callback && typeof callback === 'function') callback();
|
if (callback && typeof callback === 'function') callback();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1328,6 +1338,7 @@ function saveAs(op1, callback) {
|
|||||||
if (!guardWebSave()) return;
|
if (!guardWebSave()) return;
|
||||||
var prevOpenKey = openAs.name || '';
|
var prevOpenKey = openAs.name || '';
|
||||||
if (typeof window.soonApplySavePreview === 'function') window.soonApplySavePreview(con_o, canvas1);
|
if (typeof window.soonApplySavePreview === 'function') window.soonApplySavePreview(con_o, canvas1);
|
||||||
|
if (typeof window.soonRefreshDesignCanvases === 'function') window.soonRefreshDesignCanvases();
|
||||||
con_o.soonType = 1;
|
con_o.soonType = 1;
|
||||||
var dialogApi = (typeof dialog !== 'undefined' && dialog) ? dialog : (window.platformBridge && window.platformBridge.showSaveDialog ? { showSaveDialog: function(opts) { return window.platformBridge.showSaveDialog(opts); } } : null);
|
var dialogApi = (typeof dialog !== 'undefined' && dialog) ? dialog : (window.platformBridge && window.platformBridge.showSaveDialog ? { showSaveDialog: function(opts) { return window.platformBridge.showSaveDialog(opts); } } : null);
|
||||||
if (!dialogApi) return;
|
if (!dialogApi) return;
|
||||||
@@ -1395,6 +1406,7 @@ function save(op1, callback) {
|
|||||||
if (!guardWebSave()) return;
|
if (!guardWebSave()) return;
|
||||||
var prevOpenKey = openAs.name || '';
|
var prevOpenKey = openAs.name || '';
|
||||||
if (typeof window.soonApplySavePreview === 'function') window.soonApplySavePreview(con_o, canvas1);
|
if (typeof window.soonApplySavePreview === 'function') window.soonApplySavePreview(con_o, canvas1);
|
||||||
|
if (typeof window.soonRefreshDesignCanvases === 'function') window.soonRefreshDesignCanvases();
|
||||||
con_o.soonType = 1;
|
con_o.soonType = 1;
|
||||||
var content = JSON.stringify(con_o);
|
var content = JSON.stringify(con_o);
|
||||||
var needsDialog = typeof window.soonNeedsSaveDialog === 'function'
|
var needsDialog = typeof window.soonNeedsSaveDialog === 'function'
|
||||||
|
|||||||
@@ -552,10 +552,6 @@ function relayoutDesign2Canvas() {
|
|||||||
if (typeof window.soonDesign2ScaleBgSources === 'function') {
|
if (typeof window.soonDesign2ScaleBgSources === 'function') {
|
||||||
window.soonDesign2ScaleBgSources();
|
window.soonDesign2ScaleBgSources();
|
||||||
}
|
}
|
||||||
if (window.is_bgi_add) {
|
|
||||||
canvas1.requestRenderAll();
|
|
||||||
canvas2.requestRenderAll();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function soonDesign2ScaleBgSources() {
|
function soonDesign2ScaleBgSources() {
|
||||||
@@ -572,16 +568,6 @@ function soonDesign2ScaleBgSources() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function soonDesign2BgOverlayMetrics(imgEl) {
|
|
||||||
if (!imgEl || !imgEl.complete || imgEl.naturalWidth <= 0) return null;
|
|
||||||
var z = zoom || window.zoom || 0.5;
|
|
||||||
var w = parseFloat(imgEl.style.width);
|
|
||||||
if (!w || w <= 0) w = imgEl.naturalWidth * z;
|
|
||||||
var h = parseFloat(imgEl.style.height);
|
|
||||||
if (!h || h <= 0) h = imgEl.naturalHeight * z;
|
|
||||||
return { w: w, h: h };
|
|
||||||
}
|
|
||||||
|
|
||||||
function soonDesign2PrepareBgSources(done) {
|
function soonDesign2PrepareBgSources(done) {
|
||||||
var sf = document.getElementById('source_front');
|
var sf = document.getElementById('source_front');
|
||||||
var sb = document.getElementById('source_back');
|
var sb = document.getElementById('source_back');
|
||||||
@@ -634,7 +620,6 @@ function soonDesign2Kickoff() {
|
|||||||
|
|
||||||
window.soonDesign2ScaleBgSources = soonDesign2ScaleBgSources;
|
window.soonDesign2ScaleBgSources = soonDesign2ScaleBgSources;
|
||||||
window.soonDesign2PrepareBgSources = soonDesign2PrepareBgSources;
|
window.soonDesign2PrepareBgSources = soonDesign2PrepareBgSources;
|
||||||
window.soonDesign2BgOverlayMetrics = soonDesign2BgOverlayMetrics;
|
|
||||||
window.soonDesign2Kickoff = soonDesign2Kickoff;
|
window.soonDesign2Kickoff = soonDesign2Kickoff;
|
||||||
|
|
||||||
initRulers();
|
initRulers();
|
||||||
@@ -2037,10 +2022,10 @@ function applyFontList(data) {
|
|||||||
$('#count_font_family').append("<option value='" + item + "'>" + item + '</option>')
|
$('#count_font_family').append("<option value='" + item + "'>" + item + '</option>')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (typeof ipcRenderer !== 'undefined' && ipcRenderer) {
|
if (typeof window !== 'undefined' && window.platformBridge && window.platformBridge.getSystemFonts) {
|
||||||
ipcRenderer.on('font-list', function(event, data) { applyFontList(data); })
|
|
||||||
} else if (typeof window !== 'undefined' && window.platformBridge && window.platformBridge.getSystemFonts) {
|
|
||||||
window.platformBridge.getSystemFonts().then(applyFontList)
|
window.platformBridge.getSystemFonts().then(applyFontList)
|
||||||
|
} else if (typeof ipcRenderer !== 'undefined' && ipcRenderer) {
|
||||||
|
ipcRenderer.on('font-list', function(event, data) { applyFontList(data); })
|
||||||
}
|
}
|
||||||
function applySysLan(data) {
|
function applySysLan(data) {
|
||||||
let lang = localStorage.getItem('lang')
|
let lang = localStorage.getItem('lang')
|
||||||
|
|||||||
@@ -1096,6 +1096,8 @@ function soonPageDesignType() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function onCloudWriteDone(res, fallback, callback) {
|
function onCloudWriteDone(res, fallback, callback) {
|
||||||
|
if (typeof window.soonFinishSaveUi === 'function') window.soonFinishSaveUi();
|
||||||
|
else if (typeof window.soonCleanupLayerMask === 'function') window.soonCleanupLayerMask();
|
||||||
applyCloudWriteResult(res, fallback);
|
applyCloudWriteResult(res, fallback);
|
||||||
if (res && res.fileKey && typeof window.soonClearPendingImport === 'function') {
|
if (res && res.fileKey && typeof window.soonClearPendingImport === 'function') {
|
||||||
window.soonClearPendingImport();
|
window.soonClearPendingImport();
|
||||||
@@ -1103,7 +1105,12 @@ function onCloudWriteDone(res, fallback, callback) {
|
|||||||
if (typeof window.soonRecentOnCloudSave === 'function') {
|
if (typeof window.soonRecentOnCloudSave === 'function') {
|
||||||
window.soonRecentOnCloudSave(res, fallback, soonPageDesignType());
|
window.soonRecentOnCloudSave(res, fallback, soonPageDesignType());
|
||||||
}
|
}
|
||||||
layer.msg(language_str('saveSucc') + cloudSaveSuccessLabel(res, fallback));
|
var msg = language_str('saveSucc') + cloudSaveSuccessLabel(res, fallback);
|
||||||
|
if (res && res.syncFailed) {
|
||||||
|
msg = '已保存到本地,云端同步失败';
|
||||||
|
}
|
||||||
|
if (typeof window.soonToast === 'function') window.soonToast(msg, res && res.syncFailed ? 'warn' : 'success');
|
||||||
|
else layer.msg(msg, { shade: 0, time: 2500 });
|
||||||
if (callback && typeof callback === 'function') callback();
|
if (callback && typeof callback === 'function') callback();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1147,6 +1154,7 @@ function saveAs(op1, callback) {
|
|||||||
if (!guardWebSave()) return;
|
if (!guardWebSave()) return;
|
||||||
var prevOpenKey = openAs.name || '';
|
var prevOpenKey = openAs.name || '';
|
||||||
if (typeof window.soonApplySavePreview === 'function') window.soonApplySavePreview(con_o, canvas1);
|
if (typeof window.soonApplySavePreview === 'function') window.soonApplySavePreview(con_o, canvas1);
|
||||||
|
if (typeof window.soonRefreshDesignCanvases === 'function') window.soonRefreshDesignCanvases();
|
||||||
con_o.soonType = 2;
|
con_o.soonType = 2;
|
||||||
var dialogApi = (typeof dialog !== 'undefined' && dialog) ? dialog : (window.platformBridge && window.platformBridge.showSaveDialog ? { showSaveDialog: function(opts) { return window.platformBridge.showSaveDialog(opts); } } : null);
|
var dialogApi = (typeof dialog !== 'undefined' && dialog) ? dialog : (window.platformBridge && window.platformBridge.showSaveDialog ? { showSaveDialog: function(opts) { return window.platformBridge.showSaveDialog(opts); } } : null);
|
||||||
if (!dialogApi) return;
|
if (!dialogApi) return;
|
||||||
@@ -1166,7 +1174,8 @@ function saveAs(op1, callback) {
|
|||||||
if (fs) {
|
if (fs) {
|
||||||
fs.writeFileSync(fp, content, 'utf8');
|
fs.writeFileSync(fp, content, 'utf8');
|
||||||
openAs.name = fp;
|
openAs.name = fp;
|
||||||
layer.msg(language_str('saveSucc') + openAs.name);
|
if (typeof window.soonToast === 'function') window.soonToast(language_str('saveSucc') + openAs.name, 'success');
|
||||||
|
else layer.msg(language_str('saveSucc') + openAs.name, { shade: 0 });
|
||||||
saveHistory();
|
saveHistory();
|
||||||
if (callback && typeof callback === 'function') callback();
|
if (callback && typeof callback === 'function') callback();
|
||||||
}
|
}
|
||||||
@@ -1212,6 +1221,7 @@ function save(op1, callback) {
|
|||||||
if (!guardWebSave()) return;
|
if (!guardWebSave()) return;
|
||||||
var prevOpenKey = openAs.name || '';
|
var prevOpenKey = openAs.name || '';
|
||||||
if (typeof window.soonApplySavePreview === 'function') window.soonApplySavePreview(con_o, canvas1);
|
if (typeof window.soonApplySavePreview === 'function') window.soonApplySavePreview(con_o, canvas1);
|
||||||
|
if (typeof window.soonRefreshDesignCanvases === 'function') window.soonRefreshDesignCanvases();
|
||||||
con_o.soonType = 2;
|
con_o.soonType = 2;
|
||||||
var content = JSON.stringify(con_o);
|
var content = JSON.stringify(con_o);
|
||||||
var needsDialog = typeof window.soonNeedsSaveDialog === 'function'
|
var needsDialog = typeof window.soonNeedsSaveDialog === 'function'
|
||||||
@@ -1225,7 +1235,8 @@ function save(op1, callback) {
|
|||||||
var fs = typeof require !== 'undefined' ? require('fs') : null;
|
var fs = typeof require !== 'undefined' ? require('fs') : null;
|
||||||
if (fs) {
|
if (fs) {
|
||||||
fs.writeFileSync(openAs.name, content, 'utf8');
|
fs.writeFileSync(openAs.name, content, 'utf8');
|
||||||
layer.msg(language_str('saveSucc') + openAs.name);
|
if (typeof window.soonToast === 'function') window.soonToast(language_str('saveSucc') + openAs.name, 'success');
|
||||||
|
else layer.msg(language_str('saveSucc') + openAs.name, { shade: 0 });
|
||||||
saveHistory();
|
saveHistory();
|
||||||
if (callback && typeof callback === 'function') callback();
|
if (callback && typeof callback === 'function') callback();
|
||||||
}
|
}
|
||||||
@@ -1248,7 +1259,8 @@ function save(op1, callback) {
|
|||||||
if (fs) {
|
if (fs) {
|
||||||
fs.writeFileSync(fp, content, 'utf8');
|
fs.writeFileSync(fp, content, 'utf8');
|
||||||
openAs.name = fp;
|
openAs.name = fp;
|
||||||
layer.msg(language_str('saveSucc') + openAs.name);
|
if (typeof window.soonToast === 'function') window.soonToast(language_str('saveSucc') + openAs.name, 'success');
|
||||||
|
else layer.msg(language_str('saveSucc') + openAs.name, { shade: 0 });
|
||||||
saveHistory();
|
saveHistory();
|
||||||
if (callback && typeof callback === 'function') callback();
|
if (callback && typeof callback === 'function') callback();
|
||||||
}
|
}
|
||||||
@@ -1529,6 +1541,9 @@ window.openFile = function open(file, jAlready) {
|
|||||||
}).catch(function () {
|
}).catch(function () {
|
||||||
if (typeof window.soonReportOpenLoadError === 'function') window.soonReportOpenLoadError('模板/文件加载失败');
|
if (typeof window.soonReportOpenLoadError === 'function') window.soonReportOpenLoadError('模板/文件加载失败');
|
||||||
else if (typeof window.soonHideOpenFileLoading === 'function') window.soonHideOpenFileLoading();
|
else if (typeof window.soonHideOpenFileLoading === 'function') window.soonHideOpenFileLoading();
|
||||||
|
}).finally(function () {
|
||||||
|
if (typeof window.soonHideOpenFileLoading === 'function') window.soonHideOpenFileLoading();
|
||||||
|
if (typeof window.soonCleanupLayerMask === 'function') window.soonCleanupLayerMask();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -365,8 +365,6 @@ $('#version_change').click(function () {
|
|||||||
if (typeof window.soonDesign2ScaleBgSources === 'function') {
|
if (typeof window.soonDesign2ScaleBgSources === 'function') {
|
||||||
window.soonDesign2ScaleBgSources()
|
window.soonDesign2ScaleBgSources()
|
||||||
}
|
}
|
||||||
canvas1.requestRenderAll()
|
|
||||||
canvas2.requestRenderAll()
|
|
||||||
}
|
}
|
||||||
sf.onerror = sf.onload
|
sf.onerror = sf.onload
|
||||||
sf.src = soonAsset('front_bg' + bg_version + '_2.png')
|
sf.src = soonAsset('front_bg' + bg_version + '_2.png')
|
||||||
@@ -2703,42 +2701,10 @@ if (typeof window._design2CanvasEventsInitialized === 'undefined') {
|
|||||||
// - mouse:up (已在 core.js 中处理)
|
// - mouse:up (已在 core.js 中处理)
|
||||||
// 如果需要 canvas1 特定的处理逻辑,请修改 core.js 中的相应函数
|
// 如果需要 canvas1 特定的处理逻辑,请修改 core.js 中的相应函数
|
||||||
|
|
||||||
// 保留 canvas1 特定的 after:render 处理
|
|
||||||
canvas1.on('after:render', function () {
|
|
||||||
if (!window.is_bgi_add) return
|
|
||||||
var image = document.getElementById('source_front')
|
|
||||||
var metrics = typeof window.soonDesign2BgOverlayMetrics === 'function'
|
|
||||||
? window.soonDesign2BgOverlayMetrics(image)
|
|
||||||
: null
|
|
||||||
if (!metrics) return
|
|
||||||
ctx1.drawImage(
|
|
||||||
image,
|
|
||||||
(canvas1.width - metrics.w) / 2,
|
|
||||||
(canvas1.height - metrics.h) / 2,
|
|
||||||
metrics.w,
|
|
||||||
metrics.h
|
|
||||||
)
|
|
||||||
})
|
|
||||||
// 保留 canvas1 特定的 text:changed 处理
|
// 保留 canvas1 特定的 text:changed 处理
|
||||||
canvas1.on('text:changed', (e) => {
|
canvas1.on('text:changed', (e) => {
|
||||||
$('#text_text').val(e.target.get('text'))
|
$('#text_text').val(e.target.get('text'))
|
||||||
})
|
})
|
||||||
// 保留 canvas2 特定的 after:render 处理
|
|
||||||
canvas2.on('after:render', function () {
|
|
||||||
if (!window.is_bgi_add) return
|
|
||||||
var image = document.getElementById('source_back')
|
|
||||||
var metrics = typeof window.soonDesign2BgOverlayMetrics === 'function'
|
|
||||||
? window.soonDesign2BgOverlayMetrics(image)
|
|
||||||
: null
|
|
||||||
if (!metrics) return
|
|
||||||
ctx2.drawImage(
|
|
||||||
image,
|
|
||||||
(canvas2.width - metrics.w) / 2,
|
|
||||||
(canvas2.height - metrics.h) / 2,
|
|
||||||
metrics.w,
|
|
||||||
metrics.h
|
|
||||||
)
|
|
||||||
})
|
|
||||||
requestAnimationFrame(function () {
|
requestAnimationFrame(function () {
|
||||||
requestAnimationFrame(function () {
|
requestAnimationFrame(function () {
|
||||||
if (typeof window.soonDesign2Kickoff === 'function') {
|
if (typeof window.soonDesign2Kickoff === 'function') {
|
||||||
|
|||||||
+175
-57
@@ -47,6 +47,57 @@
|
|||||||
'Accept': 'application/json'
|
'Accept': 'application/json'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var CLOUD_JSON_LIMIT = 16 * 1024 * 1024 - 65536;
|
||||||
|
var CLOUD_STRIP_KEYS = [
|
||||||
|
'frontColorPic', 'backColorPic', 'frontBlackPic', 'backBlackPic', 'backDisplayPic'
|
||||||
|
];
|
||||||
|
var CLOUD_REGEN_IMAGE_TYPES = { 2: 1, 8: 1, 9: 1 };
|
||||||
|
|
||||||
|
function slimCloudFabricSide(data, sideKey, metaKey) {
|
||||||
|
var canvas = data[sideKey];
|
||||||
|
var metaList = data[metaKey];
|
||||||
|
if (!canvas || !Array.isArray(canvas.objects)) return;
|
||||||
|
canvas.objects.forEach(function (obj, i) {
|
||||||
|
if (!obj || typeof obj.src !== 'string') return;
|
||||||
|
var meta = Array.isArray(metaList) && i > 0 ? metaList[i - 1] : null;
|
||||||
|
var t = meta && meta.type;
|
||||||
|
if (CLOUD_REGEN_IMAGE_TYPES[t]) delete obj.src;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function slimCloudPayload(data) {
|
||||||
|
CLOUD_STRIP_KEYS.forEach(function (key) { delete data[key]; });
|
||||||
|
var preview = data.frontDisplayPic;
|
||||||
|
if (typeof preview === 'string' && preview.length > 120000) delete data.frontDisplayPic;
|
||||||
|
slimCloudFabricSide(data, 'f', 'fo');
|
||||||
|
slimCloudFabricSide(data, 'b', 'bo');
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatPayloadTooLargeMessage(sizeBytes) {
|
||||||
|
var mb = Math.max(0.1, Math.round(sizeBytes / 1024 / 1024 * 10) / 10);
|
||||||
|
return '设计文件约 ' + mb + 'MB,超过云端上传限制。请减少或压缩自定义图片;'
|
||||||
|
+ '若仍失败,请让管理员将 MySQL max_allowed_packet 调至 32M 以上。';
|
||||||
|
}
|
||||||
|
|
||||||
|
function prepareCloudJsonStr(jsonStr) {
|
||||||
|
if (typeof jsonStr !== 'string') jsonStr = String(jsonStr || '{}');
|
||||||
|
if (jsonStr.length <= CLOUD_JSON_LIMIT) return jsonStr;
|
||||||
|
var slim = jsonStr;
|
||||||
|
try {
|
||||||
|
var data = slimCloudPayload(JSON.parse(jsonStr));
|
||||||
|
slim = JSON.stringify(data);
|
||||||
|
if (slim.length <= CLOUD_JSON_LIMIT) return slim;
|
||||||
|
delete data.frontDisplayPic;
|
||||||
|
slim = JSON.stringify(data);
|
||||||
|
if (slim.length <= CLOUD_JSON_LIMIT) return slim;
|
||||||
|
} catch (e) { /* ignore */ }
|
||||||
|
var err = new Error(formatPayloadTooLargeMessage(slim.length));
|
||||||
|
err.code = 'payload_too_large';
|
||||||
|
err.status = 413;
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
function parseCloudRef(p) {
|
function parseCloudRef(p) {
|
||||||
return typeof window.soonParseFileKey === 'function' ? window.soonParseFileKey(p) : null;
|
return typeof window.soonParseFileKey === 'function' ? window.soonParseFileKey(p) : null;
|
||||||
}
|
}
|
||||||
@@ -83,13 +134,12 @@
|
|||||||
var err = new Error(info.message || 'cloud_error');
|
var err = new Error(info.message || 'cloud_error');
|
||||||
err.status = info.status;
|
err.status = info.status;
|
||||||
err.code = info.code;
|
err.code = info.code;
|
||||||
|
err.apiNotified = true;
|
||||||
return Promise.reject(err);
|
return Promise.reject(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
function requireCloudAuth(actionLabel) {
|
function requireCloudAuth(actionLabel) {
|
||||||
if (getAccessToken()) return true;
|
return !!getAccessToken();
|
||||||
if (typeof window.soonRequireLogin === 'function') return window.soonRequireLogin(actionLabel);
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function navigateToPage(pathWithQuery) {
|
function navigateToPage(pathWithQuery) {
|
||||||
@@ -106,21 +156,41 @@
|
|||||||
return 'en';
|
return 'en';
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSystemFonts() {
|
var WEB_FONT_FALLBACK = [
|
||||||
var list = [
|
'Arial', 'Arial Black', 'Comic Sans MS', 'Courier New', 'Georgia',
|
||||||
'Arial', 'Arial Black', 'Comic Sans MS', 'Courier New', 'Georgia',
|
'Impact', 'Microsoft YaHei', 'SimHei', 'SimSun', 'KaiTi', 'FangSong',
|
||||||
'Impact', 'Microsoft YaHei', 'SimHei', 'SimSun', 'KaiTi', 'FangSong',
|
'Times New Roman', 'Trebuchet MS', 'Verdana', 'PingFang SC', 'Hiragino Sans GB',
|
||||||
'Times New Roman', 'Trebuchet MS', 'Verdana', 'PingFang SC', 'Hiragino Sans GB'
|
'Helvetica Neue', 'Segoe UI', 'Tahoma', 'STHeiti', 'STSong', 'STKaiti', 'STFangsong'
|
||||||
];
|
];
|
||||||
|
|
||||||
|
function mergeFontNames() {
|
||||||
|
var set = {};
|
||||||
|
WEB_FONT_FALLBACK.forEach(function (name) { set[name] = 1; });
|
||||||
if (typeof document !== 'undefined' && document.fonts && document.fonts.forEach) {
|
if (typeof document !== 'undefined' && document.fonts && document.fonts.forEach) {
|
||||||
var set = {};
|
|
||||||
document.fonts.forEach(function (f) {
|
document.fonts.forEach(function (f) {
|
||||||
var name = (f.family || '').replace(/^["']|["']$/g, '');
|
var name = (f.family || '').replace(/^["']|["']$/g, '');
|
||||||
if (name) set[name] = 1;
|
if (name) set[name] = 1;
|
||||||
});
|
});
|
||||||
list = Object.keys(set).length ? Object.keys(set).sort() : list;
|
|
||||||
}
|
}
|
||||||
return Promise.resolve(list);
|
return Object.keys(set).sort();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSystemFonts() {
|
||||||
|
if (typeof window !== 'undefined' && window.queryLocalFonts) {
|
||||||
|
return window.queryLocalFonts().then(function (fonts) {
|
||||||
|
var set = {};
|
||||||
|
WEB_FONT_FALLBACK.forEach(function (name) { set[name] = 1; });
|
||||||
|
(fonts || []).forEach(function (f) {
|
||||||
|
var name = (f.fullName || f.family || '').replace(/^["']|["']$/g, '');
|
||||||
|
if (name) set[name] = 1;
|
||||||
|
});
|
||||||
|
var merged = Object.keys(set).sort();
|
||||||
|
return merged.length ? merged : mergeFontNames();
|
||||||
|
}).catch(function () {
|
||||||
|
return mergeFontNames();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return Promise.resolve(mergeFontNames());
|
||||||
}
|
}
|
||||||
|
|
||||||
function listCloudFiles(pageOrLimit, sizeOrOffset) {
|
function listCloudFiles(pageOrLimit, sizeOrOffset) {
|
||||||
@@ -152,6 +222,11 @@
|
|||||||
|
|
||||||
function createCloudFile(name, jsonStr) {
|
function createCloudFile(name, jsonStr) {
|
||||||
if (!requireCloudAuth('保存')) return Promise.reject(new Error('unauthorized'));
|
if (!requireCloudAuth('保存')) return Promise.reject(new Error('unauthorized'));
|
||||||
|
try {
|
||||||
|
jsonStr = prepareCloudJsonStr(jsonStr);
|
||||||
|
} catch (e) {
|
||||||
|
return Promise.reject(e);
|
||||||
|
}
|
||||||
return authedFetch('files', {
|
return authedFetch('files', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: JSON_HEADERS,
|
headers: JSON_HEADERS,
|
||||||
@@ -197,6 +272,11 @@
|
|||||||
|
|
||||||
function updateCloudFile(id, name, jsonStr, version) {
|
function updateCloudFile(id, name, jsonStr, version) {
|
||||||
if (!requireCloudAuth('保存')) return Promise.reject(new Error('unauthorized'));
|
if (!requireCloudAuth('保存')) return Promise.reject(new Error('unauthorized'));
|
||||||
|
try {
|
||||||
|
jsonStr = prepareCloudJsonStr(jsonStr);
|
||||||
|
} catch (e) {
|
||||||
|
return Promise.reject(e);
|
||||||
|
}
|
||||||
var body = { name: normalizeSoonName(name), json: jsonStr };
|
var body = { name: normalizeSoonName(name), json: jsonStr };
|
||||||
if (version != null) body.version = version;
|
if (version != null) body.version = version;
|
||||||
return authedFetch('files/' + id, {
|
return authedFetch('files/' + id, {
|
||||||
@@ -511,14 +591,23 @@
|
|||||||
var name = input && input.value ? String(input.value).trim() : defaultName;
|
var name = input && input.value ? String(input.value).trim() : defaultName;
|
||||||
name = resolveSaveDialogName(name, options, isSoon);
|
name = resolveSaveDialogName(name, options, isSoon);
|
||||||
layer.close(index);
|
layer.close(index);
|
||||||
|
if (typeof window.soonCleanupLayerMask === 'function') {
|
||||||
|
setTimeout(function () { window.soonCleanupLayerMask(); }, 0);
|
||||||
|
}
|
||||||
resolve({ canceled: false, filePath: name, useCloud: !!isSoon });
|
resolve({ canceled: false, filePath: name, useCloud: !!isSoon });
|
||||||
},
|
},
|
||||||
btn2: function (index) {
|
btn2: function (index) {
|
||||||
layer.close(index);
|
layer.close(index);
|
||||||
|
if (typeof window.soonCleanupLayerMask === 'function') {
|
||||||
|
setTimeout(function () { window.soonCleanupLayerMask(); }, 0);
|
||||||
|
}
|
||||||
resolve({ canceled: true });
|
resolve({ canceled: true });
|
||||||
},
|
},
|
||||||
cancel: function (index) {
|
cancel: function (index) {
|
||||||
layer.close(index);
|
layer.close(index);
|
||||||
|
if (typeof window.soonCleanupLayerMask === 'function') {
|
||||||
|
setTimeout(function () { window.soonCleanupLayerMask(); }, 0);
|
||||||
|
}
|
||||||
resolve({ canceled: true });
|
resolve({ canceled: true });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -563,59 +652,75 @@
|
|||||||
return cacheWriteForKey(res.fileKey, meta).then(function () { return res; });
|
return cacheWriteForKey(res.fileKey, meta).then(function () { return res; });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!getAccessToken()) {
|
function resolveLocalSaveKey() {
|
||||||
if (cloudRef && cloudRef.id) {
|
if (name.indexOf('soondesign_session:') === 0) return name;
|
||||||
requireCloudAuth('保存');
|
if (name.indexOf('soondesign_file:') === 0) return name;
|
||||||
return Promise.reject(new Error('unauthorized'));
|
if (typeof window.soonMakeSessionKey === 'function') {
|
||||||
|
return window.soonMakeSessionKey(name);
|
||||||
}
|
}
|
||||||
var sessionKey = typeof window.soonMakeSessionKey === 'function'
|
return 'soondesign_session:' + fileName.replace(/\.soon$/i, '');
|
||||||
? window.soonMakeSessionKey(name)
|
}
|
||||||
: (name.indexOf('soondesign_session:') === 0
|
|
||||||
? name
|
function localSaveMeta(localKey) {
|
||||||
: 'soondesign_session:' + fileName.replace(/\.soon$/i, ''));
|
if (localKey.indexOf('soondesign_file:') === 0) {
|
||||||
var prevSessionKey = name.indexOf('soondesign_session:') === 0 ? name : '';
|
return { source: 'cloud', name: fileName, type: parsedJson ? parsedJson.soonType : 1 };
|
||||||
function finishSessionSave() {
|
|
||||||
try { sessionStorage.setItem(sessionKey, 'idb'); } catch (e) { /* ignore */ }
|
|
||||||
return { fileKey: sessionKey, name: fileName, version: 0 };
|
|
||||||
}
|
}
|
||||||
if (prevSessionKey && prevSessionKey !== sessionKey &&
|
return { source: 'session', name: fileName, type: parsedJson ? parsedJson.soonType : 1 };
|
||||||
typeof window.soonLocalRenameKey === 'function') {
|
}
|
||||||
return cacheWriteForKey(sessionKey, { source: 'session', name: fileName }).then(function () {
|
|
||||||
return window.soonLocalRenameKey(prevSessionKey, sessionKey, { source: 'session', name: fileName })
|
function finishLocalSave(localKey) {
|
||||||
.then(finishSessionSave);
|
if (localKey.indexOf('soondesign_session:') === 0) {
|
||||||
}).catch(function () {
|
try { sessionStorage.setItem(localKey, 'idb'); } catch (e) { /* ignore */ }
|
||||||
return Promise.reject(new Error('save_failed'));
|
}
|
||||||
|
return { fileKey: localKey, name: fileName, version: 0, localOnly: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveLocalFirst() {
|
||||||
|
var localKey = resolveLocalSaveKey();
|
||||||
|
var prevKey = (name.indexOf('soondesign_session:') === 0 || name.indexOf('soondesign_file:') === 0) ? name : '';
|
||||||
|
var meta = localSaveMeta(localKey);
|
||||||
|
if (prevKey && prevKey !== localKey && typeof window.soonLocalRenameKey === 'function') {
|
||||||
|
return cacheWriteForKey(localKey, meta).then(function () {
|
||||||
|
return window.soonLocalRenameKey(prevKey, localKey, meta).then(function () {
|
||||||
|
return finishLocalSave(localKey);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return cacheWriteForKey(sessionKey, { source: 'session', name: fileName }).then(function () {
|
return cacheWriteForKey(localKey, meta).then(function () {
|
||||||
return finishSessionSave();
|
return finishLocalSave(localKey);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncCloudIfAuthed(localResult) {
|
||||||
|
if (!getAccessToken()) return Promise.resolve(localResult);
|
||||||
|
if (cloudRef && cloudRef.id) {
|
||||||
|
var ver = cloudRef.version;
|
||||||
|
if (ver == null && window._soonFileMeta && window._soonFileMeta.id === cloudRef.id) {
|
||||||
|
ver = window._soonFileMeta.version;
|
||||||
|
}
|
||||||
|
var cloudSaveName = fileName;
|
||||||
|
if (typeof window.soonResolveCloudFileName === 'function') {
|
||||||
|
var resolved = window.soonResolveCloudFileName(name);
|
||||||
|
if (resolved) cloudSaveName = resolved;
|
||||||
|
}
|
||||||
|
return updateCloudFile(cloudRef.id, cloudSaveName, str, ver).then(function (res) {
|
||||||
|
return afterCloudSave(res, localResult.fileKey || name);
|
||||||
|
}).catch(function () {
|
||||||
|
localResult.syncFailed = true;
|
||||||
|
return localResult;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
var prevKey = localResult.fileKey || name;
|
||||||
|
return createCloudFile(fileName, str).then(function (res) {
|
||||||
|
return afterCloudSave(res, prevKey);
|
||||||
}).catch(function () {
|
}).catch(function () {
|
||||||
return Promise.reject(new Error('save_failed'));
|
localResult.syncFailed = true;
|
||||||
|
return localResult;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!requireCloudAuth('保存')) {
|
return saveLocalFirst().then(syncCloudIfAuthed).catch(function () {
|
||||||
return Promise.reject(new Error('unauthorized'));
|
return Promise.reject(new Error('save_failed'));
|
||||||
}
|
|
||||||
|
|
||||||
if (cloudRef && cloudRef.id) {
|
|
||||||
var ver = cloudRef.version;
|
|
||||||
if (ver == null && window._soonFileMeta && window._soonFileMeta.id === cloudRef.id) {
|
|
||||||
ver = window._soonFileMeta.version;
|
|
||||||
}
|
|
||||||
if (typeof window.soonResolveCloudFileName === 'function') {
|
|
||||||
var cloudSaveName = window.soonResolveCloudFileName(name);
|
|
||||||
if (cloudSaveName) fileName = cloudSaveName;
|
|
||||||
}
|
|
||||||
var prevCloudKey = name;
|
|
||||||
return updateCloudFile(cloudRef.id, fileName, str, ver).then(function (res) {
|
|
||||||
return afterCloudSave(res, prevCloudKey);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
var prevKey = name.indexOf('soondesign_session:') === 0 ? name : '';
|
|
||||||
return createCloudFile(fileName, str).then(function (res) {
|
|
||||||
return afterCloudSave(res, prevKey);
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
readFile: function (pathOrHandle) {
|
readFile: function (pathOrHandle) {
|
||||||
@@ -713,6 +818,11 @@
|
|||||||
window.ipcRenderer = {
|
window.ipcRenderer = {
|
||||||
send: function (ch, a1, a2) {
|
send: function (ch, a1, a2) {
|
||||||
if (ch === 'get-sys-language') { window._sysLanPending = true; setTimeout(function () { if (window._sysLanCb && window._sysLanPending) { window._sysLanCb(null, loc); window._sysLanPending = false; } }, 0); }
|
if (ch === 'get-sys-language') { window._sysLanPending = true; setTimeout(function () { if (window._sysLanCb && window._sysLanPending) { window._sysLanCb(null, loc); window._sysLanPending = false; } }, 0); }
|
||||||
|
if (ch === 'get-sys-fonts' && bridge.getSystemFonts) {
|
||||||
|
bridge.getSystemFonts().then(function (fonts) {
|
||||||
|
if (typeof window._fontListCb === 'function') window._fontListCb(null, fonts || []);
|
||||||
|
});
|
||||||
|
}
|
||||||
if (ch === 'open-first-page' && bridge.openFirstPage) { bridge.openFirstPage(); }
|
if (ch === 'open-first-page' && bridge.openFirstPage) { bridge.openFirstPage(); }
|
||||||
if (ch === 'open-design-page' && bridge.openDesignPage && (a1 !== undefined || a2 !== undefined)) { bridge.openDesignPage(a1 || '', a2 || 1); }
|
if (ch === 'open-design-page' && bridge.openDesignPage && (a1 !== undefined || a2 !== undefined)) { bridge.openDesignPage(a1 || '', a2 || 1); }
|
||||||
if (ch === 'open-help-file' && bridge.openHelp) { bridge.openHelp(); }
|
if (ch === 'open-help-file' && bridge.openHelp) { bridge.openHelp(); }
|
||||||
@@ -720,6 +830,14 @@
|
|||||||
},
|
},
|
||||||
on: function (ch, cb) {
|
on: function (ch, cb) {
|
||||||
if (ch === 'sys-lan') { window._sysLanCb = cb; if (window._sysLanPending) setTimeout(function () { if (window._sysLanCb) { window._sysLanCb(null, loc); window._sysLanPending = false; } }, 0); }
|
if (ch === 'sys-lan') { window._sysLanCb = cb; if (window._sysLanPending) setTimeout(function () { if (window._sysLanCb) { window._sysLanCb(null, loc); window._sysLanPending = false; } }, 0); }
|
||||||
|
if (ch === 'font-list') {
|
||||||
|
window._fontListCb = cb;
|
||||||
|
if (bridge.getSystemFonts) {
|
||||||
|
bridge.getSystemFonts().then(function (fonts) {
|
||||||
|
if (typeof window._fontListCb === 'function') window._fontListCb(null, fonts || []);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
if (ch === 'close') window._closeCb = cb;
|
if (ch === 'close') window._closeCb = cb;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -486,12 +486,45 @@ body.soon-admin-page .soon-admin-date-wrap input[type="date"].soon-input::-webki
|
|||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
.soon-admin-form { padding: var(--soon-space-md); }
|
.soon-admin-form { padding: var(--soon-space-md); }
|
||||||
|
.soon-admin-form--modal-shell {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
.soon-admin-form__scroll {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: var(--soon-space-md);
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: rgba(255, 255, 255, 0.18) transparent;
|
||||||
|
}
|
||||||
|
.soon-admin-form__scroll::-webkit-scrollbar { width: 8px; }
|
||||||
|
.soon-admin-form__scroll::-webkit-scrollbar-thumb {
|
||||||
|
background: rgba(255, 255, 255, 0.16);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.soon-layer-content--form-fixed .soon-admin-form--modal-shell {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
.soon-admin-form--modal-shell.soon-admin-form--plan .soon-admin-form__scroll,
|
||||||
|
.soon-admin-tpl-modal.soon-admin-form--modal-shell .soon-admin-form__scroll {
|
||||||
|
padding: var(--soon-space-lg);
|
||||||
|
}
|
||||||
.soon-admin-form--plan {
|
.soon-admin-form--plan {
|
||||||
padding: var(--soon-space-lg);
|
padding: var(--soon-space-lg);
|
||||||
}
|
}
|
||||||
.soon-admin-form--plan .soon-admin-form-panel:last-of-type {
|
.soon-admin-form--plan .soon-admin-form-panel:last-of-type {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
.soon-admin-form--modal-shell .soon-admin-form-panel:last-of-type {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
.soon-admin-form-grid {
|
.soon-admin-form-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: 1fr 1fr;
|
||||||
@@ -677,6 +710,22 @@ body.soon-admin-page .soon-admin-date-wrap input[type="date"].soon-input::-webki
|
|||||||
padding-top: var(--soon-space-md);
|
padding-top: var(--soon-space-md);
|
||||||
border-top: 1px solid var(--soon-border);
|
border-top: 1px solid var(--soon-border);
|
||||||
}
|
}
|
||||||
|
.soon-admin-form__foot {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: var(--soon-space-sm);
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-top: 0;
|
||||||
|
padding: var(--soon-space-md) var(--soon-space-lg);
|
||||||
|
border-top: 1px solid var(--soon-border);
|
||||||
|
background: var(--soon-bg-elevated);
|
||||||
|
}
|
||||||
|
.soon-admin-tpl-modal.soon-admin-form--modal-shell {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.soon-admin-tpl-modal__actions.soon-admin-form__foot {
|
||||||
|
padding-top: var(--soon-space-md);
|
||||||
|
}
|
||||||
|
|
||||||
.soon-admin-filter-bar {
|
.soon-admin-filter-bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -141,10 +141,62 @@
|
|||||||
setBtnLoading(btn, true, opts.loadingLabel);
|
setBtnLoading(btn, true, opts.loadingLabel);
|
||||||
ret.finally(function () {
|
ret.finally(function () {
|
||||||
setBtnLoading(btn, false);
|
setBtnLoading(btn, false);
|
||||||
|
if (typeof window.soonCleanupLayerMask === 'function') window.soonCleanupLayerMask();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function layerContentEl(layero) {
|
||||||
|
if (layero && typeof layero.find === 'function') {
|
||||||
|
return layero.find('.layui-layer-content')[0] || null;
|
||||||
|
}
|
||||||
|
if (layero && layero.querySelector) {
|
||||||
|
return layero.querySelector('.layui-layer-content');
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 弹窗表单:内容区滚动,保存按钮固定底栏 */
|
||||||
|
function layoutAdminFormModal(contentEl, opts) {
|
||||||
|
if (!contentEl || contentEl.dataset.soonFormLayout) return;
|
||||||
|
opts = opts || {};
|
||||||
|
var root = contentEl.querySelector('.soon-admin-form, .soon-admin-tpl-modal');
|
||||||
|
if (!root) return;
|
||||||
|
|
||||||
|
var foot = root.querySelector('.soon-admin-form__actions, .soon-admin-tpl-modal__actions');
|
||||||
|
if (!foot) {
|
||||||
|
var saveBtn = root.querySelector('#adminModalSave');
|
||||||
|
if (!saveBtn) return;
|
||||||
|
foot = document.createElement('div');
|
||||||
|
foot.className = 'soon-admin-form__actions';
|
||||||
|
saveBtn.parentNode.insertBefore(foot, saveBtn);
|
||||||
|
foot.appendChild(saveBtn);
|
||||||
|
}
|
||||||
|
|
||||||
|
var scroll = document.createElement('div');
|
||||||
|
scroll.className = 'soon-admin-form__scroll';
|
||||||
|
while (root.firstChild && root.firstChild !== foot) {
|
||||||
|
scroll.appendChild(root.firstChild);
|
||||||
|
}
|
||||||
|
if (!scroll.childNodes.length) return;
|
||||||
|
|
||||||
|
root.insertBefore(scroll, foot);
|
||||||
|
root.classList.add('soon-admin-form--modal-shell');
|
||||||
|
foot.classList.add('soon-admin-form__foot');
|
||||||
|
|
||||||
|
contentEl.classList.add('soon-layer-content--form');
|
||||||
|
var area = opts.area || ['420px', 'auto'];
|
||||||
|
if (area[1] && area[1] !== 'auto') {
|
||||||
|
contentEl.classList.add('soon-layer-content--form-fixed');
|
||||||
|
contentEl.style.height = area[1];
|
||||||
|
contentEl.style.maxHeight = area[1];
|
||||||
|
} else if (opts.maxHeight) {
|
||||||
|
contentEl.classList.add('soon-layer-content--form-fixed');
|
||||||
|
contentEl.style.maxHeight = opts.maxHeight;
|
||||||
|
}
|
||||||
|
contentEl.dataset.soonFormLayout = '1';
|
||||||
|
}
|
||||||
|
|
||||||
function openFormModal(title, formHtml, onSave, opts) {
|
function openFormModal(title, formHtml, onSave, opts) {
|
||||||
if (typeof layer === 'undefined') return;
|
if (typeof layer === 'undefined') return;
|
||||||
opts = opts || {};
|
opts = opts || {};
|
||||||
@@ -156,16 +208,8 @@
|
|||||||
maxWidth: opts.maxWidth || '96vw',
|
maxWidth: opts.maxWidth || '96vw',
|
||||||
content: formHtml,
|
content: formHtml,
|
||||||
success: function (layero) {
|
success: function (layero) {
|
||||||
var area = opts.area || ['420px', 'auto'];
|
var contentEl = layerContentEl(layero);
|
||||||
if (area[1] && area[1] !== 'auto') {
|
layoutAdminFormModal(contentEl, opts);
|
||||||
var contentEl = null;
|
|
||||||
if (layero && typeof layero.find === 'function') {
|
|
||||||
contentEl = layero.find('.layui-layer-content')[0];
|
|
||||||
} else if (layero && layero.querySelector) {
|
|
||||||
contentEl = layero.querySelector('.layui-layer-content');
|
|
||||||
}
|
|
||||||
if (contentEl) contentEl.classList.add('soon-layer-content--scroll');
|
|
||||||
}
|
|
||||||
var btn = document.getElementById('adminModalSave');
|
var btn = document.getElementById('adminModalSave');
|
||||||
if (btn) {
|
if (btn) {
|
||||||
btn.onclick = function () {
|
btn.onclick = function () {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
var Api = function () { return window.AdminApi; };
|
var Api = function () { return window.AdminApi; };
|
||||||
var Ui = function () { return window.AdminUi; };
|
var Ui = function () { return window.AdminUi; };
|
||||||
var I = function () { return window.AdminI18n; };
|
var I = function () { return window.AdminI18n; };
|
||||||
|
var Form = function () { return window.AdminForm; };
|
||||||
var State = function () { return window.AdminState; };
|
var State = function () { return window.AdminState; };
|
||||||
var Filter = function () { return window.AdminFilter; };
|
var Filter = function () { return window.AdminFilter; };
|
||||||
var Menu = function () { return window.AdminMenu; };
|
var Menu = function () { return window.AdminMenu; };
|
||||||
@@ -162,7 +163,7 @@
|
|||||||
'<div class="soon-form-field"><label class="soon-form-label">备注</label>' +
|
'<div class="soon-form-field"><label class="soon-form-label">备注</label>' +
|
||||||
'<textarea id="orderNote" class="soon-input soon-input--sm" rows="3" placeholder="' + I().esc(placeholder) +
|
'<textarea id="orderNote" class="soon-input soon-input--sm" rows="3" placeholder="' + I().esc(placeholder) +
|
||||||
'"></textarea></div>' +
|
'"></textarea></div>' +
|
||||||
'<button type="button" class="soon-btn soon-btn--sm" id="adminModalSave">确认</button></div>';
|
Form().actions('确认', 'adminModalSave') + '</div>';
|
||||||
Ui().openFormModal(title, form, function () {
|
Ui().openFormModal(title, form, function () {
|
||||||
return onSave(Ui().el('orderNote').value.trim());
|
return onSave(Ui().el('orderNote').value.trim());
|
||||||
});
|
});
|
||||||
@@ -246,7 +247,7 @@
|
|||||||
openNoteModal(title, '可选:填写审核备注', function (note) {
|
openNoteModal(title, '可选:填写审核备注', function (note) {
|
||||||
return Api().postJson('orders/' + btn.dataset.id + '/refund/approve', { note: note }).then(function (r) {
|
return Api().postJson('orders/' + btn.dataset.id + '/refund/approve', { note: note }).then(function (r) {
|
||||||
if (r) {
|
if (r) {
|
||||||
layer.closeAll();
|
soonLayerDismiss();
|
||||||
var gw = r.data && r.data.gateway;
|
var gw = r.data && r.data.gateway;
|
||||||
var msg = '退款已完成';
|
var msg = '退款已完成';
|
||||||
if (gw && gw.indexOf('skipped') === 0) msg += '(未配置支付密钥,仅标记退款并撤销会员)';
|
if (gw && gw.indexOf('skipped') === 0) msg += '(未配置支付密钥,仅标记退款并撤销会员)';
|
||||||
@@ -268,7 +269,7 @@
|
|||||||
}
|
}
|
||||||
return Api().postJson('orders/' + btn.dataset.id + '/refund/reject', { note: note }).then(function (r) {
|
return Api().postJson('orders/' + btn.dataset.id + '/refund/reject', { note: note }).then(function (r) {
|
||||||
if (r) {
|
if (r) {
|
||||||
layer.closeAll();
|
soonLayerDismiss();
|
||||||
soonToast('已拒绝退款申请', 'success');
|
soonToast('已拒绝退款申请', 'success');
|
||||||
Ui().closeDrawer();
|
Ui().closeDrawer();
|
||||||
render();
|
render();
|
||||||
|
|||||||
@@ -192,7 +192,7 @@
|
|||||||
};
|
};
|
||||||
return Api().postJson('plans', body).then(function (r) {
|
return Api().postJson('plans', body).then(function (r) {
|
||||||
if (r) {
|
if (r) {
|
||||||
layer.closeAll();
|
soonLayerDismiss();
|
||||||
soonToast('已保存', 'success');
|
soonToast('已保存', 'success');
|
||||||
render();
|
render();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -87,19 +87,20 @@
|
|||||||
var form = '<div class="soon-admin-form">' +
|
var form = '<div class="soon-admin-form">' +
|
||||||
Form().field('配置键', '<input class="soon-input soon-input--sm" value="' + I().esc(key) + '" disabled>') +
|
Form().field('配置键', '<input class="soon-input soon-input--sm" value="' + I().esc(key) + '" disabled>') +
|
||||||
Form().field('配置值', Form().input('settingVal', { value: val, type: 'textarea', rows: 5 })) +
|
Form().field('配置值', Form().input('settingVal', { value: val, type: 'textarea', rows: 5 })) +
|
||||||
'<button type="button" class="soon-btn soon-btn--sm" id="adminModalSave">保存</button></div>';
|
Form().actions('保存', 'adminModalSave') + '</div>';
|
||||||
Ui().openFormModal('修改 ' + key, form, function () {
|
Ui().openFormModal('修改 ' + key, form, function () {
|
||||||
Ui().confirm('确认保存该配置?', function () {
|
Ui().confirm('确认保存该配置?', function () {
|
||||||
var saveBtn = Ui().el('adminModalSave');
|
var saveBtn = Ui().el('adminModalSave');
|
||||||
Ui().setBtnLoading(saveBtn, true);
|
Ui().setBtnLoading(saveBtn, true);
|
||||||
Api().postJson('settings', { key: key, value: Ui().el('settingVal').value }).then(function (r) {
|
Api().postJson('settings', { key: key, value: Ui().el('settingVal').value }).then(function (r) {
|
||||||
if (r) {
|
if (r) {
|
||||||
layer.closeAll();
|
soonLayerDismiss();
|
||||||
soonToast('配置已保存', 'success');
|
soonToast('配置已保存', 'success');
|
||||||
render();
|
render();
|
||||||
}
|
}
|
||||||
}).finally(function () {
|
}).finally(function () {
|
||||||
Ui().setBtnLoading(saveBtn, false);
|
Ui().setBtnLoading(saveBtn, false);
|
||||||
|
if (typeof window.soonCleanupLayerMask === 'function') window.soonCleanupLayerMask();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -284,7 +284,7 @@
|
|||||||
}).then(function (r) {
|
}).then(function (r) {
|
||||||
if (r) {
|
if (r) {
|
||||||
revokeBlob(modalState);
|
revokeBlob(modalState);
|
||||||
layer.closeAll();
|
soonLayerDismiss();
|
||||||
soonToast('已上传', 'success');
|
soonToast('已上传', 'success');
|
||||||
render();
|
render();
|
||||||
}
|
}
|
||||||
@@ -307,7 +307,7 @@
|
|||||||
return Api().putJson('templates/' + plan.id, body).then(function (r) {
|
return Api().putJson('templates/' + plan.id, body).then(function (r) {
|
||||||
if (r) {
|
if (r) {
|
||||||
revokeBlob(modalState);
|
revokeBlob(modalState);
|
||||||
layer.closeAll();
|
soonLayerDismiss();
|
||||||
soonToast('已保存', 'success');
|
soonToast('已保存', 'success');
|
||||||
render();
|
render();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
var Filter = function () { return window.AdminFilter; };
|
var Filter = function () { return window.AdminFilter; };
|
||||||
var Menu = function () { return window.AdminMenu; };
|
var Menu = function () { return window.AdminMenu; };
|
||||||
var Router = function () { return window.AdminRouter; };
|
var Router = function () { return window.AdminRouter; };
|
||||||
|
var Form = function () { return window.AdminForm; };
|
||||||
|
|
||||||
function buildQuery() {
|
function buildQuery() {
|
||||||
var st = State().users;
|
var st = State().users;
|
||||||
@@ -57,7 +58,7 @@
|
|||||||
? formField('管理级别', '<div id="adminLevelField" style="display:none"><select id="ucAdminLevel" class="soon-input soon-input--sm">' +
|
? formField('管理级别', '<div id="adminLevelField" style="display:none"><select id="ucAdminLevel" class="soon-input soon-input--sm">' +
|
||||||
adminLevelOptions('ops') + '</select></div>', '运营管理员不可访问设置、审计与支付密钥')
|
adminLevelOptions('ops') + '</select></div>', '运营管理员不可访问设置、审计与支付密钥')
|
||||||
: '') +
|
: '') +
|
||||||
'<button type="button" class="soon-btn soon-btn--sm" id="adminModalSave">创建</button></div>';
|
Form().actions('创建', 'adminModalSave') + '</div>';
|
||||||
Ui().openFormModal('新增用户', form, function () {
|
Ui().openFormModal('新增用户', form, function () {
|
||||||
var email = Ui().el('ucEmail').value.trim();
|
var email = Ui().el('ucEmail').value.trim();
|
||||||
var pwd = Ui().el('ucPwd').value;
|
var pwd = Ui().el('ucPwd').value;
|
||||||
@@ -76,7 +77,7 @@
|
|||||||
}
|
}
|
||||||
return Api().postJson('users', body).then(function (r) {
|
return Api().postJson('users', body).then(function (r) {
|
||||||
if (r) {
|
if (r) {
|
||||||
layer.closeAll();
|
soonLayerDismiss();
|
||||||
soonToast('用户已创建', 'success');
|
soonToast('用户已创建', 'success');
|
||||||
render();
|
render();
|
||||||
}
|
}
|
||||||
@@ -95,7 +96,7 @@
|
|||||||
? formField('管理级别', '<div id="adminLevelField"><select id="ueAdminLevel" class="soon-input soon-input--sm">' +
|
? formField('管理级别', '<div id="adminLevelField"><select id="ueAdminLevel" class="soon-input soon-input--sm">' +
|
||||||
adminLevelOptions(level) + '</select></div>', '仅对管理员账号生效')
|
adminLevelOptions(level) + '</select></div>', '仅对管理员账号生效')
|
||||||
: '') +
|
: '') +
|
||||||
'<button type="button" class="soon-btn soon-btn--sm" id="adminModalSave">保存</button></div>';
|
Form().actions('保存', 'adminModalSave') + '</div>';
|
||||||
Ui().openFormModal('编辑用户 #' + user.id, form, function () {
|
Ui().openFormModal('编辑用户 #' + user.id, form, function () {
|
||||||
var body = {
|
var body = {
|
||||||
email: Ui().el('ueEmail').value.trim(),
|
email: Ui().el('ueEmail').value.trim(),
|
||||||
@@ -107,7 +108,7 @@
|
|||||||
}
|
}
|
||||||
return Api().putJson('users/' + user.id, body).then(function (r) {
|
return Api().putJson('users/' + user.id, body).then(function (r) {
|
||||||
if (r) {
|
if (r) {
|
||||||
layer.closeAll();
|
soonLayerDismiss();
|
||||||
soonToast('用户信息已更新', 'success');
|
soonToast('用户信息已更新', 'success');
|
||||||
render();
|
render();
|
||||||
}
|
}
|
||||||
@@ -121,7 +122,7 @@
|
|||||||
var form = '<div class="soon-admin-form">' +
|
var form = '<div class="soon-admin-form">' +
|
||||||
formField('新密码', '<input id="upPwd" type="password" class="soon-input soon-input--sm" autocomplete="new-password" placeholder="至少 8 位">') +
|
formField('新密码', '<input id="upPwd" type="password" class="soon-input soon-input--sm" autocomplete="new-password" placeholder="至少 8 位">') +
|
||||||
formField('确认密码', '<input id="upPwd2" type="password" class="soon-input soon-input--sm" autocomplete="new-password">') +
|
formField('确认密码', '<input id="upPwd2" type="password" class="soon-input soon-input--sm" autocomplete="new-password">') +
|
||||||
'<button type="button" class="soon-btn soon-btn--sm" id="adminModalSave">重置密码</button></div>';
|
Form().actions('重置密码', 'adminModalSave') + '</div>';
|
||||||
Ui().openFormModal('重置密码', form, function () {
|
Ui().openFormModal('重置密码', form, function () {
|
||||||
var p1 = Ui().el('upPwd').value;
|
var p1 = Ui().el('upPwd').value;
|
||||||
var p2 = Ui().el('upPwd2').value;
|
var p2 = Ui().el('upPwd2').value;
|
||||||
@@ -135,7 +136,7 @@
|
|||||||
}
|
}
|
||||||
return Api().postJson('users/' + userId + '/password', { password: p1 }).then(function (r) {
|
return Api().postJson('users/' + userId + '/password', { password: p1 }).then(function (r) {
|
||||||
if (r) {
|
if (r) {
|
||||||
layer.closeAll();
|
soonLayerDismiss();
|
||||||
soonToast('密码已重置', 'success');
|
soonToast('密码已重置', 'success');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -153,7 +154,7 @@
|
|||||||
formField('套餐', '<select id="usPlan" class="soon-input soon-input--sm">' + opts + '</select>') +
|
formField('套餐', '<select id="usPlan" class="soon-input soon-input--sm">' + opts + '</select>') +
|
||||||
formField('有效天数', '<input id="usDays" type="number" min="0" class="soon-input soon-input--sm" placeholder="留空则用套餐默认天数">') +
|
formField('有效天数', '<input id="usDays" type="number" min="0" class="soon-input soon-input--sm" placeholder="留空则用套餐默认天数">') +
|
||||||
'<p class="soon-admin-form-hint">选择免费版将取消当前有效会员。</p>' +
|
'<p class="soon-admin-form-hint">选择免费版将取消当前有效会员。</p>' +
|
||||||
'<button type="button" class="soon-btn soon-btn--sm" id="adminModalSave">保存</button></div>';
|
Form().actions('保存', 'adminModalSave') + '</div>';
|
||||||
Ui().openFormModal('设置会员', form, function () {
|
Ui().openFormModal('设置会员', form, function () {
|
||||||
var planSel = Ui().el('usPlan');
|
var planSel = Ui().el('usPlan');
|
||||||
var daysVal = Ui().el('usDays').value.trim();
|
var daysVal = Ui().el('usDays').value.trim();
|
||||||
@@ -161,7 +162,7 @@
|
|||||||
if (daysVal) body.days = parseInt(daysVal, 10);
|
if (daysVal) body.days = parseInt(daysVal, 10);
|
||||||
return Api().postJson('users/' + userId + '/subscription', body).then(function (r) {
|
return Api().postJson('users/' + userId + '/subscription', body).then(function (r) {
|
||||||
if (r) {
|
if (r) {
|
||||||
layer.closeAll();
|
soonLayerDismiss();
|
||||||
soonToast('会员已更新', 'success');
|
soonToast('会员已更新', 'success');
|
||||||
render();
|
render();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -139,6 +139,7 @@
|
|||||||
<script src="../js/common/auth-redirect.js"></script>
|
<script src="../js/common/auth-redirect.js"></script>
|
||||||
|
|
||||||
<script src="../js/common/auth-session.js"></script>
|
<script src="../js/common/auth-session.js"></script>
|
||||||
|
<script src="../js/common/soon-toast.js"></script>
|
||||||
<script src="../js/common/cloud-files.js"></script>
|
<script src="../js/common/cloud-files.js"></script>
|
||||||
<script src="../js/common/soon-local-store.js"></script>
|
<script src="../js/common/soon-local-store.js"></script>
|
||||||
<script src="../js/common/soon-recent.js"></script>
|
<script src="../js/common/soon-recent.js"></script>
|
||||||
|
|||||||
Reference in New Issue
Block a user