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`,行号与路径须真实。
- **中文 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`)。
## 改动
+1
View File
@@ -25,3 +25,4 @@ backend-web/storage/templates/*
*.pem
*.key
/soonDesign-back
/info
+2 -2
View File
@@ -8,7 +8,7 @@
|---|---|
| [frontend-electron/](frontend-electron/) | 桌面程序 |
| [frontend-web/](frontend-web/) | Web 前端 |
| [backend-web/](backend-web/) | PHP API |
| [backend-web/](backend-web/) | PHP API(含 `database/schema.sql` |
| [docker/](docker/) | 本地 Docker 编排(非生产部署) |
## 本地开发
@@ -39,7 +39,7 @@ scripts\start.bat
- Web 根目录:`frontend-web/`
- 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`
- 前端 API`frontend-web/config/local.js`(生产配置,勿覆盖)
+18 -2
View File
@@ -33,10 +33,26 @@ vi config/local.php
`db``jwt_secret``site``storage` 路径。
```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\Db;
use Soon\Api\Core\Json;
use PDOException;
use Throwable;
/**
* .soon 文件服务:JSON 数据流模型,乐观锁、配额、软删除。
*/
final class FileService
{
private const MAX_JSON_BYTES = 33554432;
public static function usersRoot(): string
{
$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
{
$maxFiles = MembershipService::maxFilesLimit($userId);
if (self::fileCount($userId) >= $maxFiles) {
Json::fail('file_limit_exceeded', '已达文件数量上限(' . $maxFiles . ' 个),请清理文件或续订', 413);
$name = self::normalizeName($name);
self::assertJsonPayload($json);
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
{
$name = self::normalizeName($name);
self::assertJsonPayload($json);
$newSize = strlen($json);
$quota = self::quotaBytes($userId);
$used = self::usedBytes($userId);
@@ -121,7 +133,7 @@ final class FileService
}
$now = date('Y-m-d H:i:s');
$newVersion = (int)$row['version'] + 1;
$thumb = TemplateService::thumbFromSoonJson($json);
$thumb = self::extractThumb($json);
$upd = $pdo->prepare(
'UPDATE soon_files SET name = :n, json = :j, size = :s, version = :v, thumb = :th, updated_at = :ts '
. 'WHERE id = :id AND version = :cv'
@@ -137,17 +149,77 @@ final class FileService
$pdo->commit();
return ['id' => $id, 'name' => $name, 'size' => $newSize, 'version' => $newVersion, 'updated_at' => $now];
} catch (\RuntimeException $e) {
if ($pdo->inTransaction()) $pdo->rollBack();
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);
if ($pdo->inTransaction()) {
$pdo->rollBack();
}
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;
} catch (\Throwable $e) {
if ($pdo->inTransaction()) $pdo->rollBack();
} catch (PDOException $e) {
if ($pdo->inTransaction()) {
$pdo->rollBack();
}
self::failDb($e);
} catch (Throwable $e) {
if ($pdo->inTransaction()) {
$pdo->rollBack();
}
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
{
$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';
\Soon\Api\Services\PaymentConfigLoader::reload($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
volumes:
- 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:
test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-uroot", "-p${MYSQL_ROOT_PASSWORD:-rootlocal}"]
interval: 5s
+3 -144
View File
@@ -1,6 +1,8 @@
<?php
declare(strict_types=1);
require dirname(__DIR__, 2) . '/backend-web/scripts/schema-migrate-lib.php';
$host = getenv('SOON_DB_HOST') ?: 'mysql';
$port = (int)(getenv('SOON_DB_PORT') ?: 3306);
$user = getenv('SOON_DB_USER') ?: 'soondesign';
@@ -14,151 +16,8 @@ try {
$pass,
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
);
soon_schema_migrate($pdo, 'migrate-dev-schema');
} catch (Throwable $e) {
fwrite(STDERR, 'migrate-dev-schema: ' . $e->getMessage() . PHP_EOL);
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;
color: #ffffff;
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;
}
.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 {
overflow-x: hidden !important;
+1
View File
@@ -6,3 +6,4 @@ window.SOON_DEPLOY_CONFIG = {
web_base: 'https://design.cardsoon.com',
version: '1.0.0'
};
+7 -2
View File
@@ -85,16 +85,21 @@
fullUrl = getApiBase() + '/' + String(url).replace(/^\/+/, '');
}
var hadSession = !!(token || getRefreshToken());
return fetch(fullUrl, opts).then(function (response) {
if (response.status !== 401) return response;
if (!getRefreshToken()) return response;
return refreshAccessToken().then(function (newToken) {
var retryHeaders = Object.assign({}, opts.headers || {});
retryHeaders['Authorization'] = 'Bearer ' + newToken;
var retryOpts = Object.assign({}, opts, { headers: retryHeaders });
return fetch(fullUrl, retryOpts);
}).catch(function () {
clearSession();
redirectToLogin();
if (hadSession) {
clearSession();
redirectToLogin();
}
return response;
});
});
+75 -25
View File
@@ -88,22 +88,29 @@
}
});
if (hidden.length) canvas.renderAll();
window._soonSkipDesign1CdMask = true;
var mult = 0.18;
var quality = 0.72;
var url = '';
for (var i = 0; i < 3; i++) {
try {
var candidate = canvas.toDataURL({ format: 'jpeg', quality: quality, multiplier: mult });
if (candidate && candidate.indexOf('data:image/') === 0 && candidate.length <= THUMB_MAX) {
url = candidate;
break;
}
} catch (e) { /* ignore */ }
quality -= 0.18;
mult -= 0.04;
try {
for (var i = 0; i < 3; i++) {
try {
var candidate = canvas.toDataURL({ format: 'jpeg', quality: quality, multiplier: mult });
if (candidate && candidate.indexOf('data:image/') === 0 && candidate.length <= THUMB_MAX) {
url = candidate;
break;
}
} catch (e) { /* ignore */ }
quality -= 0.18;
mult -= 0.04;
}
} finally {
window._soonSkipDesign1CdMask = false;
}
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;
}
@@ -134,8 +141,20 @@
return soonEnsureSoonExt(name);
}
function dismissLayerMask() {
if (typeof window.soonCleanupLayerMask === 'function') {
window.soonCleanupLayerMask();
return;
}
soonHideOpenFileLoading();
invokeLayerCleanup();
}
function soonReportSaveError(err) {
if (typeof window.soonFinishSaveUi === 'function') window.soonFinishSaveUi();
else dismissLayerMask();
if (err && err.conflictSynced) return;
if (err && err.apiNotified) return;
if (typeof window.soonShowApiError === 'function') {
window.soonShowApiError({
status: err && err.status,
@@ -149,11 +168,27 @@
function soonWriteSoonContentWeb(fp, content, prevOpenKey, onDone, onError) {
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);
}).catch(function (err) {
if (typeof onError === 'function') onError(err);
else soonReportSaveError(err);
}).finally(function () {
dismissLayerMask();
});
return true;
}
@@ -656,37 +691,52 @@
});
}
function invokeLayerCleanup() {
if (typeof window.soonCleanupLayerMask === 'function') {
window.soonCleanupLayerMask();
}
}
function soonReportOpenLoadError(message) {
soonHideOpenFileLoading();
invokeLayerCleanup();
soonToast(message || '文件加载失败', 'error');
}
function soonShowOpenFileLoading() {
if (typeof layer === 'undefined' || !layer.load) return;
soonHideOpenFileLoading();
window._soonOpenLoadIdx = layer.load(1, { shade: [0.12, '#000'] });
window._soonOpenLoadIdx = layer.load(1, { shade: 0 });
}
function soonHideOpenFileLoading() {
if (window._soonOpenLoadIdx == null) return;
if (typeof layer !== 'undefined' && layer.close) layer.close(window._soonOpenLoadIdx);
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;
invokeLayerCleanup();
}
function 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;
var label = actionLabel || '继续';
soonToast('请先登录后再' + label, 'warn');
if (typeof window.soonRedirectToLogin === 'function') {
setTimeout(function () { window.soonRedirectToLogin(); }, 600);
} else if (typeof window.redirectToLogin === 'function') {
setTimeout(function () { window.redirectToLogin(); }, 600);
} else {
window.location.href = 'login.web.html';
soonToast('请先登录后再' + (label || '操作'), 'warn');
if (options.redirect) {
if (typeof window.soonRedirectToLogin === 'function') {
setTimeout(function () { window.soonRedirectToLogin(); }, 600);
} else if (typeof window.redirectToLogin === 'function') {
setTimeout(function () { window.redirectToLogin(); }, 600);
} else {
window.location.href = 'login.web.html';
}
}
return false;
}
@@ -724,8 +774,8 @@
else if (info.status === 409) kind = 'warn';
else if (info.status === 413) kind = 'warn';
soonToast(info.message, kind);
if (info.status === 401) {
soonRequireLogin('操作');
if (info.status === 401 && info.requireLogin) {
soonRequireLogin('操作', { redirect: true });
}
}
@@ -79,6 +79,7 @@
if (idx != null) layer.close(idx);
if (payIdx != null) layer.close(payIdx);
}
if (typeof window.soonCleanupLayerMask === 'function') window.soonCleanupLayerMask();
if (fireStay && opts && typeof opts.onStay === 'function') opts.onStay();
}
@@ -93,6 +94,7 @@
_gate.payIndex = null;
if (typeof layer !== 'undefined' && gateIdx != null) layer.close(gateIdx);
_gate = { index: null, payIndex: null, plan: null, gateOpts: null };
if (typeof window.soonCleanupLayerMask === 'function') window.soonCleanupLayerMask();
toast('恭喜您,会员已激活!', 'success');
if (typeof window.soonLoadMembership === 'function') {
window.soonLoadMembership(true).then(function () {
@@ -69,6 +69,7 @@
if (idx != null && typeof layer !== 'undefined') {
try { layer.close(idx); } catch (e) { /* ignore */ }
}
if (typeof window.soonCleanupLayerMask === 'function') window.soonCleanupLayerMask();
}
function bindModal(modal) {
@@ -607,6 +607,7 @@
displayChannels: channels,
onPaid: function () {
layer.close(idx);
if (typeof window.soonCleanupLayerMask === 'function') window.soonCleanupLayerMask();
if (typeof opts.onPaid === 'function') opts.onPaid();
},
onBack: opts.onBack,
@@ -617,6 +618,7 @@
},
end: function () {
if (payCtrl) payCtrl.destroy();
if (typeof window.soonCleanupLayerMask === 'function') window.soonCleanupLayerMask();
if (typeof opts.onClose === 'function') opts.onClose();
},
});
+3
View File
@@ -17,6 +17,9 @@
localStorage.removeItem('soon_access');
localStorage.removeItem('soon_refresh');
}
if (typeof window.soonApplyMembership === 'function') {
window.soonApplyMembership(null);
}
}
function initClearCache() {
+73 -1
View File
@@ -1,14 +1,81 @@
(function () {
'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) {
if (typeof layer === 'undefined' || !layer.msg) return;
var skin = 'soon-toast';
if (type === 'success') skin += ' soon-toast--success';
else if (type === 'error') skin += ' soon-toast--error';
else if (type === 'warn') skin += ' soon-toast--warn';
var time = type === 'success' ? 2500 : (type === 'error' ? 4200 : 3600);
layer.msg(message, {
time: 2400,
time: time,
shade: 0,
offset: '72px',
icon: -1,
@@ -16,5 +83,10 @@
});
}
window.soonCleanupLayerMask = soonCleanupLayerMask;
window.soonLayerDismiss = soonLayerDismiss;
window.soonClearFabricOverlay = soonClearFabricOverlay;
window.soonRefreshDesignCanvases = soonRefreshDesignCanvases;
window.soonFinishSaveUi = soonFinishSaveUi;
window.soonToast = soonToast;
})();
+4 -4
View File
@@ -102,10 +102,10 @@ function applyFontList(data) {
$("#count_font_family").append("<option value='" + item + "'>" + item + "</option>");
}
}
if (typeof ipcRenderer !== 'undefined' && ipcRenderer) {
ipcRenderer.on('font-list', function(event, data) { applyFontList(data); });
} else if (typeof window !== 'undefined' && window.platformBridge && window.platformBridge.getSystemFonts) {
if (typeof window !== 'undefined' && window.platformBridge && window.platformBridge.getSystemFonts) {
window.platformBridge.getSystemFonts().then(applyFontList);
} else if (typeof ipcRenderer !== 'undefined' && ipcRenderer) {
ipcRenderer.on('font-list', function(event, data) { applyFontList(data); });
}
function applySysLan(data) {
let lang = localStorage.getItem("lang");
@@ -1816,7 +1816,7 @@ function initCanvasEvents() {
const ctx = currentCanvas.getContext();
let bgObj = (currentCanvas === canvas1) ? background_image1 : background_image2;
if (bgObj) {
if (bgObj && !window._soonSkipDesign1CdMask) {
const width = currentCanvas.width;
const height = currentCanvas.height;
+13 -1
View File
@@ -1204,6 +1204,9 @@ window.openFile = function open(file, jAlready) {
}).catch(function () {
if (typeof window.soonReportOpenLoadError === 'function') window.soonReportOpenLoadError('模板/文件加载失败');
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 {
@@ -1277,6 +1280,8 @@ function soonPageDesignType() {
}
function onCloudWriteDone(res, fallback, callback) {
if (typeof window.soonFinishSaveUi === 'function') window.soonFinishSaveUi();
else if (typeof window.soonCleanupLayerMask === 'function') window.soonCleanupLayerMask();
applyCloudWriteResult(res, fallback);
if (res && res.fileKey && typeof window.soonClearPendingImport === 'function') {
window.soonClearPendingImport();
@@ -1284,7 +1289,12 @@ function onCloudWriteDone(res, fallback, callback) {
if (typeof window.soonRecentOnCloudSave === 'function') {
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();
}
@@ -1328,6 +1338,7 @@ function saveAs(op1, callback) {
if (!guardWebSave()) return;
var prevOpenKey = openAs.name || '';
if (typeof window.soonApplySavePreview === 'function') window.soonApplySavePreview(con_o, canvas1);
if (typeof window.soonRefreshDesignCanvases === 'function') window.soonRefreshDesignCanvases();
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);
if (!dialogApi) return;
@@ -1395,6 +1406,7 @@ function save(op1, callback) {
if (!guardWebSave()) return;
var prevOpenKey = openAs.name || '';
if (typeof window.soonApplySavePreview === 'function') window.soonApplySavePreview(con_o, canvas1);
if (typeof window.soonRefreshDesignCanvases === 'function') window.soonRefreshDesignCanvases();
con_o.soonType = 1;
var content = JSON.stringify(con_o);
var needsDialog = typeof window.soonNeedsSaveDialog === 'function'
+3 -18
View File
@@ -552,10 +552,6 @@ function relayoutDesign2Canvas() {
if (typeof window.soonDesign2ScaleBgSources === 'function') {
window.soonDesign2ScaleBgSources();
}
if (window.is_bgi_add) {
canvas1.requestRenderAll();
canvas2.requestRenderAll();
}
}
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) {
var sf = document.getElementById('source_front');
var sb = document.getElementById('source_back');
@@ -634,7 +620,6 @@ function soonDesign2Kickoff() {
window.soonDesign2ScaleBgSources = soonDesign2ScaleBgSources;
window.soonDesign2PrepareBgSources = soonDesign2PrepareBgSources;
window.soonDesign2BgOverlayMetrics = soonDesign2BgOverlayMetrics;
window.soonDesign2Kickoff = soonDesign2Kickoff;
initRulers();
@@ -2037,10 +2022,10 @@ function applyFontList(data) {
$('#count_font_family').append("<option value='" + item + "'>" + item + '</option>')
}
}
if (typeof ipcRenderer !== 'undefined' && ipcRenderer) {
ipcRenderer.on('font-list', function(event, data) { applyFontList(data); })
} else if (typeof window !== 'undefined' && window.platformBridge && window.platformBridge.getSystemFonts) {
if (typeof window !== 'undefined' && window.platformBridge && window.platformBridge.getSystemFonts) {
window.platformBridge.getSystemFonts().then(applyFontList)
} else if (typeof ipcRenderer !== 'undefined' && ipcRenderer) {
ipcRenderer.on('font-list', function(event, data) { applyFontList(data); })
}
function applySysLan(data) {
let lang = localStorage.getItem('lang')
+19 -4
View File
@@ -1096,6 +1096,8 @@ function soonPageDesignType() {
}
function onCloudWriteDone(res, fallback, callback) {
if (typeof window.soonFinishSaveUi === 'function') window.soonFinishSaveUi();
else if (typeof window.soonCleanupLayerMask === 'function') window.soonCleanupLayerMask();
applyCloudWriteResult(res, fallback);
if (res && res.fileKey && typeof window.soonClearPendingImport === 'function') {
window.soonClearPendingImport();
@@ -1103,7 +1105,12 @@ function onCloudWriteDone(res, fallback, callback) {
if (typeof window.soonRecentOnCloudSave === 'function') {
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();
}
@@ -1147,6 +1154,7 @@ function saveAs(op1, callback) {
if (!guardWebSave()) return;
var prevOpenKey = openAs.name || '';
if (typeof window.soonApplySavePreview === 'function') window.soonApplySavePreview(con_o, canvas1);
if (typeof window.soonRefreshDesignCanvases === 'function') window.soonRefreshDesignCanvases();
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);
if (!dialogApi) return;
@@ -1166,7 +1174,8 @@ function saveAs(op1, callback) {
if (fs) {
fs.writeFileSync(fp, content, 'utf8');
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();
if (callback && typeof callback === 'function') callback();
}
@@ -1212,6 +1221,7 @@ function save(op1, callback) {
if (!guardWebSave()) return;
var prevOpenKey = openAs.name || '';
if (typeof window.soonApplySavePreview === 'function') window.soonApplySavePreview(con_o, canvas1);
if (typeof window.soonRefreshDesignCanvases === 'function') window.soonRefreshDesignCanvases();
con_o.soonType = 2;
var content = JSON.stringify(con_o);
var needsDialog = typeof window.soonNeedsSaveDialog === 'function'
@@ -1225,7 +1235,8 @@ function save(op1, callback) {
var fs = typeof require !== 'undefined' ? require('fs') : null;
if (fs) {
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();
if (callback && typeof callback === 'function') callback();
}
@@ -1248,7 +1259,8 @@ function save(op1, callback) {
if (fs) {
fs.writeFileSync(fp, content, 'utf8');
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();
if (callback && typeof callback === 'function') callback();
}
@@ -1529,6 +1541,9 @@ window.openFile = function open(file, jAlready) {
}).catch(function () {
if (typeof window.soonReportOpenLoadError === 'function') window.soonReportOpenLoadError('模板/文件加载失败');
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 {
-34
View File
@@ -365,8 +365,6 @@ $('#version_change').click(function () {
if (typeof window.soonDesign2ScaleBgSources === 'function') {
window.soonDesign2ScaleBgSources()
}
canvas1.requestRenderAll()
canvas2.requestRenderAll()
}
sf.onerror = sf.onload
sf.src = soonAsset('front_bg' + bg_version + '_2.png')
@@ -2703,42 +2701,10 @@ if (typeof window._design2CanvasEventsInitialized === 'undefined') {
// - mouse:up (已在 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.on('text:changed', (e) => {
$('#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 () {
if (typeof window.soonDesign2Kickoff === 'function') {
+175 -57
View File
@@ -47,6 +47,57 @@
'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) {
return typeof window.soonParseFileKey === 'function' ? window.soonParseFileKey(p) : null;
}
@@ -83,13 +134,12 @@
var err = new Error(info.message || 'cloud_error');
err.status = info.status;
err.code = info.code;
err.apiNotified = true;
return Promise.reject(err);
}
function requireCloudAuth(actionLabel) {
if (getAccessToken()) return true;
if (typeof window.soonRequireLogin === 'function') return window.soonRequireLogin(actionLabel);
return false;
return !!getAccessToken();
}
function navigateToPage(pathWithQuery) {
@@ -106,21 +156,41 @@
return 'en';
}
function getSystemFonts() {
var list = [
'Arial', 'Arial Black', 'Comic Sans MS', 'Courier New', 'Georgia',
'Impact', 'Microsoft YaHei', 'SimHei', 'SimSun', 'KaiTi', 'FangSong',
'Times New Roman', 'Trebuchet MS', 'Verdana', 'PingFang SC', 'Hiragino Sans GB'
];
var WEB_FONT_FALLBACK = [
'Arial', 'Arial Black', 'Comic Sans MS', 'Courier New', 'Georgia',
'Impact', 'Microsoft YaHei', 'SimHei', 'SimSun', 'KaiTi', 'FangSong',
'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) {
var set = {};
document.fonts.forEach(function (f) {
var name = (f.family || '').replace(/^["']|["']$/g, '');
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) {
@@ -152,6 +222,11 @@
function createCloudFile(name, jsonStr) {
if (!requireCloudAuth('保存')) return Promise.reject(new Error('unauthorized'));
try {
jsonStr = prepareCloudJsonStr(jsonStr);
} catch (e) {
return Promise.reject(e);
}
return authedFetch('files', {
method: 'POST',
headers: JSON_HEADERS,
@@ -197,6 +272,11 @@
function updateCloudFile(id, name, jsonStr, version) {
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 };
if (version != null) body.version = version;
return authedFetch('files/' + id, {
@@ -511,14 +591,23 @@
var name = input && input.value ? String(input.value).trim() : defaultName;
name = resolveSaveDialogName(name, options, isSoon);
layer.close(index);
if (typeof window.soonCleanupLayerMask === 'function') {
setTimeout(function () { window.soonCleanupLayerMask(); }, 0);
}
resolve({ canceled: false, filePath: name, useCloud: !!isSoon });
},
btn2: function (index) {
layer.close(index);
if (typeof window.soonCleanupLayerMask === 'function') {
setTimeout(function () { window.soonCleanupLayerMask(); }, 0);
}
resolve({ canceled: true });
},
cancel: function (index) {
layer.close(index);
if (typeof window.soonCleanupLayerMask === 'function') {
setTimeout(function () { window.soonCleanupLayerMask(); }, 0);
}
resolve({ canceled: true });
}
});
@@ -563,59 +652,75 @@
return cacheWriteForKey(res.fileKey, meta).then(function () { return res; });
}
if (!getAccessToken()) {
if (cloudRef && cloudRef.id) {
requireCloudAuth('保存');
return Promise.reject(new Error('unauthorized'));
function resolveLocalSaveKey() {
if (name.indexOf('soondesign_session:') === 0) return name;
if (name.indexOf('soondesign_file:') === 0) return name;
if (typeof window.soonMakeSessionKey === 'function') {
return window.soonMakeSessionKey(name);
}
var sessionKey = typeof window.soonMakeSessionKey === 'function'
? window.soonMakeSessionKey(name)
: (name.indexOf('soondesign_session:') === 0
? name
: 'soondesign_session:' + fileName.replace(/\.soon$/i, ''));
var prevSessionKey = name.indexOf('soondesign_session:') === 0 ? name : '';
function finishSessionSave() {
try { sessionStorage.setItem(sessionKey, 'idb'); } catch (e) { /* ignore */ }
return { fileKey: sessionKey, name: fileName, version: 0 };
return 'soondesign_session:' + fileName.replace(/\.soon$/i, '');
}
function localSaveMeta(localKey) {
if (localKey.indexOf('soondesign_file:') === 0) {
return { source: 'cloud', name: fileName, type: parsedJson ? parsedJson.soonType : 1 };
}
if (prevSessionKey && prevSessionKey !== sessionKey &&
typeof window.soonLocalRenameKey === 'function') {
return cacheWriteForKey(sessionKey, { source: 'session', name: fileName }).then(function () {
return window.soonLocalRenameKey(prevSessionKey, sessionKey, { source: 'session', name: fileName })
.then(finishSessionSave);
}).catch(function () {
return Promise.reject(new Error('save_failed'));
return { source: 'session', name: fileName, type: parsedJson ? parsedJson.soonType : 1 };
}
function finishLocalSave(localKey) {
if (localKey.indexOf('soondesign_session:') === 0) {
try { sessionStorage.setItem(localKey, 'idb'); } catch (e) { /* ignore */ }
}
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 finishSessionSave();
return cacheWriteForKey(localKey, meta).then(function () {
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 () {
return Promise.reject(new Error('save_failed'));
localResult.syncFailed = true;
return localResult;
});
}
if (!requireCloudAuth('保存')) {
return Promise.reject(new Error('unauthorized'));
}
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);
return saveLocalFirst().then(syncCloudIfAuthed).catch(function () {
return Promise.reject(new Error('save_failed'));
});
},
readFile: function (pathOrHandle) {
@@ -713,6 +818,11 @@
window.ipcRenderer = {
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-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-design-page' && bridge.openDesignPage && (a1 !== undefined || a2 !== undefined)) { bridge.openDesignPage(a1 || '', a2 || 1); }
if (ch === 'open-help-file' && bridge.openHelp) { bridge.openHelp(); }
@@ -720,6 +830,14 @@
},
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 === '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;
}
};
@@ -486,12 +486,45 @@ body.soon-admin-page .soon-admin-date-wrap input[type="date"].soon-input::-webki
border-radius: 4px;
}
.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 {
padding: var(--soon-space-lg);
}
.soon-admin-form--plan .soon-admin-form-panel:last-of-type {
margin-bottom: 0;
}
.soon-admin-form--modal-shell .soon-admin-form-panel:last-of-type {
margin-bottom: 0;
}
.soon-admin-form-grid {
display: grid;
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);
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 {
display: flex;
+54 -10
View File
@@ -141,10 +141,62 @@
setBtnLoading(btn, true, opts.loadingLabel);
ret.finally(function () {
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) {
if (typeof layer === 'undefined') return;
opts = opts || {};
@@ -156,16 +208,8 @@
maxWidth: opts.maxWidth || '96vw',
content: formHtml,
success: function (layero) {
var area = opts.area || ['420px', 'auto'];
if (area[1] && area[1] !== 'auto') {
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 contentEl = layerContentEl(layero);
layoutAdminFormModal(contentEl, opts);
var btn = document.getElementById('adminModalSave');
if (btn) {
btn.onclick = function () {
@@ -4,6 +4,7 @@
var Api = function () { return window.AdminApi; };
var Ui = function () { return window.AdminUi; };
var I = function () { return window.AdminI18n; };
var Form = function () { return window.AdminForm; };
var State = function () { return window.AdminState; };
var Filter = function () { return window.AdminFilter; };
var Menu = function () { return window.AdminMenu; };
@@ -162,7 +163,7 @@
'<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></div>' +
'<button type="button" class="soon-btn soon-btn--sm" id="adminModalSave">确认</button></div>';
Form().actions('确认', 'adminModalSave') + '</div>';
Ui().openFormModal(title, form, function () {
return onSave(Ui().el('orderNote').value.trim());
});
@@ -246,7 +247,7 @@
openNoteModal(title, '可选:填写审核备注', function (note) {
return Api().postJson('orders/' + btn.dataset.id + '/refund/approve', { note: note }).then(function (r) {
if (r) {
layer.closeAll();
soonLayerDismiss();
var gw = r.data && r.data.gateway;
var 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) {
if (r) {
layer.closeAll();
soonLayerDismiss();
soonToast('已拒绝退款申请', 'success');
Ui().closeDrawer();
render();
@@ -192,7 +192,7 @@
};
return Api().postJson('plans', body).then(function (r) {
if (r) {
layer.closeAll();
soonLayerDismiss();
soonToast('已保存', 'success');
render();
}
@@ -87,19 +87,20 @@
var form = '<div class="soon-admin-form">' +
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 })) +
'<button type="button" class="soon-btn soon-btn--sm" id="adminModalSave">保存</button></div>';
Form().actions('保存', 'adminModalSave') + '</div>';
Ui().openFormModal('修改 ' + key, form, function () {
Ui().confirm('确认保存该配置?', function () {
var saveBtn = Ui().el('adminModalSave');
Ui().setBtnLoading(saveBtn, true);
Api().postJson('settings', { key: key, value: Ui().el('settingVal').value }).then(function (r) {
if (r) {
layer.closeAll();
soonLayerDismiss();
soonToast('配置已保存', 'success');
render();
}
}).finally(function () {
Ui().setBtnLoading(saveBtn, false);
if (typeof window.soonCleanupLayerMask === 'function') window.soonCleanupLayerMask();
});
});
});
@@ -284,7 +284,7 @@
}).then(function (r) {
if (r) {
revokeBlob(modalState);
layer.closeAll();
soonLayerDismiss();
soonToast('已上传', 'success');
render();
}
@@ -307,7 +307,7 @@
return Api().putJson('templates/' + plan.id, body).then(function (r) {
if (r) {
revokeBlob(modalState);
layer.closeAll();
soonLayerDismiss();
soonToast('已保存', 'success');
render();
}
@@ -8,6 +8,7 @@
var Filter = function () { return window.AdminFilter; };
var Menu = function () { return window.AdminMenu; };
var Router = function () { return window.AdminRouter; };
var Form = function () { return window.AdminForm; };
function buildQuery() {
var st = State().users;
@@ -57,7 +58,7 @@
? formField('管理级别', '<div id="adminLevelField" style="display:none"><select id="ucAdminLevel" class="soon-input soon-input--sm">' +
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 () {
var email = Ui().el('ucEmail').value.trim();
var pwd = Ui().el('ucPwd').value;
@@ -76,7 +77,7 @@
}
return Api().postJson('users', body).then(function (r) {
if (r) {
layer.closeAll();
soonLayerDismiss();
soonToast('用户已创建', 'success');
render();
}
@@ -95,7 +96,7 @@
? formField('管理级别', '<div id="adminLevelField"><select id="ueAdminLevel" class="soon-input soon-input--sm">' +
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 () {
var body = {
email: Ui().el('ueEmail').value.trim(),
@@ -107,7 +108,7 @@
}
return Api().putJson('users/' + user.id, body).then(function (r) {
if (r) {
layer.closeAll();
soonLayerDismiss();
soonToast('用户信息已更新', 'success');
render();
}
@@ -121,7 +122,7 @@
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="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 () {
var p1 = Ui().el('upPwd').value;
var p2 = Ui().el('upPwd2').value;
@@ -135,7 +136,7 @@
}
return Api().postJson('users/' + userId + '/password', { password: p1 }).then(function (r) {
if (r) {
layer.closeAll();
soonLayerDismiss();
soonToast('密码已重置', 'success');
}
});
@@ -153,7 +154,7 @@
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="留空则用套餐默认天数">') +
'<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 () {
var planSel = Ui().el('usPlan');
var daysVal = Ui().el('usDays').value.trim();
@@ -161,7 +162,7 @@
if (daysVal) body.days = parseInt(daysVal, 10);
return Api().postJson('users/' + userId + '/subscription', body).then(function (r) {
if (r) {
layer.closeAll();
soonLayerDismiss();
soonToast('会员已更新', 'success');
render();
}
+1
View File
@@ -139,6 +139,7 @@
<script src="../js/common/auth-redirect.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/soon-local-store.js"></script>
<script src="../js/common/soon-recent.js"></script>