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:
24kycj
2026-06-24 19:24:34 +08:00
parent 4d19d7e814
commit 5fb269cc0a
36 changed files with 894 additions and 358 deletions
+1 -1
View File
@@ -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`)。
## 改动 ## 改动
+1
View File
@@ -25,3 +25,4 @@ backend-web/storage/templates/*
*.pem *.pem
*.key *.key
/soonDesign-back /soonDesign-back
/info
+2 -2
View File
@@ -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`(生产配置,勿覆盖)
+18 -2
View File
@@ -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
```
## 初始化管理员 ## 初始化管理员
+24
View File
@@ -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);
}
+160
View File
@@ -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;
}
+53
View File
@@ -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);
}
}
+100 -28
View File
@@ -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');
+2
View File
@@ -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();
+1 -1
View File
@@ -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
+3 -144
View File
@@ -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);
+11
View File
@@ -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);
} }
+9
View File
@@ -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;
+9 -8
View File
@@ -1,8 +1,9 @@
// 复制为同目录 local.js(全量更新 frontend 时不要覆盖 local.js // 复制为同目录 local.js(全量更新 frontend 时不要覆盖 local.js
window.SOON_DEPLOY_CONFIG = {
window.SOON_DEPLOY_CONFIG = { api_v1_base: 'https://designadmin.cardsoon.com/api/v1',
api_admin_base: 'https://designadmin.cardsoon.com/api/admin',
api_v1_base: 'https://designadmin.cardsoon.com/api/v1', front_base_url: 'https://design.cardsoon.com',
web_base: 'https://design.cardsoon.com',
api_admin_base: 'https://designadmin.cardsoon.com/api/admin', version: '1.0.0'
};
+7 -2
View File
@@ -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;
}); });
}); });
+75 -25
View File
@@ -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();
}, },
}); });
+3
View File
@@ -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() {
+73 -1
View File
@@ -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;
})(); })();
+4 -4
View File
@@ -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;
+13 -1
View File
@@ -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'
+3 -18
View File
@@ -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')
+19 -4
View File
@@ -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 {
-34
View File
@@ -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
View File
@@ -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;
+54 -10
View File
@@ -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();
} }
+1
View File
@@ -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>