Web 端数据交互与 design2 布局修复
统一门户 JSON 契约:新增 GET /templates/{id},模板/云文件/保存/打开走虚拟 key;
layer 保存弹层替代 prompt,本地 .soon 已登录后台 POST 登记;修复保存静默失败与 design2 全宽布局。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -60,6 +60,7 @@ Router::get('/api/admin/plans', [PlansController::class, 'list']);
|
||||
Router::post('/api/admin/plans', [PlansController::class, 'upsert']);
|
||||
|
||||
Router::get('/api/admin/templates', [TemplatesController::class, 'list']);
|
||||
Router::get('/api/admin/templates/{id}/thumb', [TemplatesController::class, 'thumb']);
|
||||
Router::post('/api/admin/templates', [TemplatesController::class, 'create']);
|
||||
Router::put('/api/admin/templates/{id}', [TemplatesController::class, 'update']);
|
||||
Router::delete('/api/admin/templates/{id}', [TemplatesController::class, 'delete']);
|
||||
|
||||
@@ -46,6 +46,7 @@ Router::get('/api/v1/auth/me', [AuthController::class, 'me']);
|
||||
|
||||
Router::get('/api/v1/files', [FileController::class, 'index']);
|
||||
Router::post('/api/v1/files', [FileController::class, 'create']);
|
||||
Router::get('/api/v1/files/{id}', [FileController::class, 'show']);
|
||||
Router::put('/api/v1/files/{id}', [FileController::class, 'update']);
|
||||
Router::delete('/api/v1/files/{id}', [FileController::class, 'delete']);
|
||||
Router::get('/api/v1/files/{id}/download', [FileController::class, 'download']);
|
||||
@@ -65,6 +66,7 @@ Router::post('/api/v1/pay/wechat/notify', [PayController::class, 'wechatNotify']
|
||||
Router::get('/api/v1/templates', [TemplateController::class, 'index']);
|
||||
Router::get('/api/v1/templates/{id}/thumb', [TemplateController::class, 'thumb']);
|
||||
Router::get('/api/v1/templates/{id}/file', [TemplateController::class, 'file']);
|
||||
Router::get('/api/v1/templates/{id}', [TemplateController::class, 'show']);
|
||||
Router::get('/api/v1/soon-models/files/{name}', [SoonModelController::class, 'download']);
|
||||
Router::get('/api/v1/soon-models', [TemplateController::class, 'index']);
|
||||
Router::get('/api/v1/settings', [SettingsController::class, 'publicSettings']);
|
||||
|
||||
@@ -51,4 +51,10 @@ final class TemplatesController
|
||||
AuditService::log($adminId, 'templates.delete', 'soon_templates:' . $id);
|
||||
Json::ok(['id' => $id]);
|
||||
}
|
||||
|
||||
public function thumb(int $adminId, int $id): void
|
||||
{
|
||||
AuditService::log($adminId, 'templates.thumb', 'soon_templates:' . $id);
|
||||
TemplateService::outputThumbAdmin($id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ namespace Soon\Api\Controllers;
|
||||
use Soon\Api\Core\Json;
|
||||
use Soon\Api\Middleware\Auth;
|
||||
use Soon\Api\Services\AuthService;
|
||||
use Soon\Api\Services\MembershipService;
|
||||
|
||||
final class AuthController
|
||||
{
|
||||
@@ -38,7 +39,6 @@ final class AuthController
|
||||
public function me(): void
|
||||
{
|
||||
$user = Auth::require();
|
||||
unset($user['password_hash']);
|
||||
Json::ok($user);
|
||||
Json::ok(array_merge($user, MembershipService::membershipSummary((int)$user['id'])));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,6 +44,20 @@ final class FileController
|
||||
Json::ok(FileService::create($u['id'], $name, $json));
|
||||
}
|
||||
|
||||
public function show(int $id): void
|
||||
{
|
||||
$u = Auth::require();
|
||||
$row = FileService::fetch($u['id'], $id);
|
||||
Json::ok([
|
||||
'id' => (int)$row['id'],
|
||||
'name' => (string)$row['name'],
|
||||
'version' => (int)$row['version'],
|
||||
'size' => (int)$row['size'],
|
||||
'updated_at' => $row['updated_at'],
|
||||
'json' => (string)$row['json'],
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(int $id): void
|
||||
{
|
||||
$u = Auth::require();
|
||||
|
||||
@@ -17,9 +17,6 @@ final class PlanController
|
||||
public function myPlan(): void
|
||||
{
|
||||
$u = Auth::require();
|
||||
Json::ok([
|
||||
'membership' => MembershipService::currentPlan($u['id']),
|
||||
'recent_orders' => MembershipService::recentOrders($u['id']),
|
||||
]);
|
||||
Json::ok(['membership' => MembershipService::currentPlan($u['id'])]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,10 +10,17 @@ final class TemplateController
|
||||
{
|
||||
public function index(): void
|
||||
{
|
||||
header('Cache-Control: public, max-age=60, must-revalidate');
|
||||
$items = TemplateService::listPublic();
|
||||
Json::ok(['items' => $items, 'total' => count($items)]);
|
||||
}
|
||||
|
||||
public function show(int $id): void
|
||||
{
|
||||
header('Cache-Control: public, max-age=60, must-revalidate');
|
||||
Json::ok(TemplateService::fetchPublicJson($id));
|
||||
}
|
||||
|
||||
public function thumb(int $id): void
|
||||
{
|
||||
TemplateService::outputThumb($id);
|
||||
|
||||
@@ -16,7 +16,7 @@ final class MembershipService
|
||||
$stmt = Db::pdo()->query(
|
||||
'SELECT id, code, name, description, price_cents, quota_mb, max_files, duration_days, '
|
||||
. 'features, sort_order, is_recommended, is_active FROM plans '
|
||||
. 'WHERE is_active = 1 AND code = "member_lifetime" AND price_cents > 0 '
|
||||
. 'WHERE is_active = 1 AND code = \'member_lifetime\' AND price_cents > 0 '
|
||||
. 'ORDER BY sort_order ASC, price_cents ASC'
|
||||
);
|
||||
$items = [];
|
||||
@@ -34,7 +34,7 @@ final class MembershipService
|
||||
$stmt = $pdo->prepare(
|
||||
'SELECT p.*, s.id AS subscription_id, s.expires_at AS subscription_expires_at, s.started_at AS subscription_started_at '
|
||||
. 'FROM subscriptions s JOIN plans p ON p.id = s.plan_id '
|
||||
. 'WHERE s.user_id = :u AND s.status = "active" AND s.expires_at > NOW() '
|
||||
. 'WHERE s.user_id = :u AND s.status = \'active\' AND s.expires_at > NOW() '
|
||||
. 'ORDER BY s.expires_at DESC LIMIT 1'
|
||||
);
|
||||
$stmt->execute(['u' => $userId]);
|
||||
@@ -70,7 +70,7 @@ final class MembershipService
|
||||
$memberQuota = (int)Config::get('limits.member_quota_mb', 2048);
|
||||
$memberFiles = (int)Config::get('limits.member_max_files', 200);
|
||||
$freeQuota = (int)Config::get('limits.free_quota_mb', $memberQuota);
|
||||
$freeStmt = $pdo->prepare('SELECT * FROM plans WHERE code = "free" AND is_active = 1 LIMIT 1');
|
||||
$freeStmt = $pdo->prepare('SELECT * FROM plans WHERE code = \'free\' AND is_active = 1 LIMIT 1');
|
||||
$freeStmt->execute();
|
||||
$freeRow = $freeStmt->fetch();
|
||||
if ($freeRow) {
|
||||
@@ -108,13 +108,32 @@ final class MembershipService
|
||||
{
|
||||
$stmt = Db::pdo()->prepare(
|
||||
'SELECT 1 FROM subscriptions s JOIN plans p ON p.id = s.plan_id '
|
||||
. 'WHERE s.user_id = :u AND s.status = "active" AND s.expires_at > NOW() '
|
||||
. 'AND p.code <> "free" AND p.price_cents > 0 LIMIT 1'
|
||||
. 'WHERE s.user_id = :u AND s.status = \'active\' AND s.expires_at > NOW() '
|
||||
. 'AND p.code <> \'free\' AND p.price_cents > 0 LIMIT 1'
|
||||
);
|
||||
$stmt->execute(['u' => $userId]);
|
||||
return (bool)$stmt->fetchColumn();
|
||||
}
|
||||
|
||||
/** @return array{is_member:bool,tier:string,name:string,subscription:array<string,mixed>} */
|
||||
public static function membershipSummary(int $userId): array
|
||||
{
|
||||
if (self::isActiveMember($userId)) {
|
||||
return [
|
||||
'is_member' => true,
|
||||
'tier' => 'member',
|
||||
'name' => '会员',
|
||||
'subscription' => ['status' => 'active'],
|
||||
];
|
||||
}
|
||||
return [
|
||||
'is_member' => false,
|
||||
'tier' => 'free',
|
||||
'name' => '普通用户',
|
||||
'subscription' => ['status' => 'free'],
|
||||
];
|
||||
}
|
||||
|
||||
/** @return array<int, array<string, mixed>> */
|
||||
public static function recentOrders(int $userId, int $limit = 8): array
|
||||
{
|
||||
|
||||
@@ -28,7 +28,7 @@ final class TemplateService
|
||||
public static function listPublic(): array
|
||||
{
|
||||
$stmt = Db::pdo()->query(
|
||||
'SELECT id, name, type FROM soon_templates WHERE is_active = 1 '
|
||||
'SELECT id, name, type, updated_at FROM soon_templates WHERE is_active = 1 '
|
||||
. 'ORDER BY sort_order ASC, id ASC'
|
||||
);
|
||||
$items = [];
|
||||
@@ -37,6 +37,7 @@ final class TemplateService
|
||||
'id' => (int)$row['id'],
|
||||
'name' => (string)$row['name'],
|
||||
'type' => (int)$row['type'],
|
||||
'updated_at' => (string)$row['updated_at'],
|
||||
];
|
||||
}
|
||||
return $items;
|
||||
@@ -93,23 +94,141 @@ final class TemplateService
|
||||
if (!is_array($data)) {
|
||||
Json::fail('bad_request', '模板须为有效的 JSON(.soon)', 400);
|
||||
}
|
||||
return self::metaFromSoonData($data);
|
||||
}
|
||||
|
||||
/** @param array<string, mixed> $data @return array{type:int, thumb:string} */
|
||||
private static function metaFromSoonData(array $data): array
|
||||
{
|
||||
$type = 1;
|
||||
if (!empty($data['soonType']) && (int)$data['soonType'] === 2) {
|
||||
$type = 2;
|
||||
} elseif (!empty($data['backBlackPic'])) {
|
||||
$type = 2;
|
||||
}
|
||||
return ['type' => $type, 'thumb' => self::extractThumb($data)];
|
||||
}
|
||||
|
||||
$thumb = '';
|
||||
if (!empty($data['frontDisplayPic']) && is_string($data['frontDisplayPic'])) {
|
||||
$candidate = trim($data['frontDisplayPic']);
|
||||
if (str_starts_with($candidate, 'data:image/') && strlen($candidate) <= self::THUMB_MAX_BYTES) {
|
||||
$thumb = $candidate;
|
||||
/** @return array{type:int, thumb:string} */
|
||||
private static function parseSoonMeta(string $json): array
|
||||
{
|
||||
$json = trim($json);
|
||||
if ($json === '') {
|
||||
return ['type' => 1, 'thumb' => ''];
|
||||
}
|
||||
$data = json_decode($json, true);
|
||||
if (!is_array($data)) {
|
||||
return ['type' => 1, 'thumb' => ''];
|
||||
}
|
||||
return self::metaFromSoonData($data);
|
||||
}
|
||||
|
||||
public static function ensureThumbStored(int $id): string
|
||||
{
|
||||
$row = self::find($id);
|
||||
if (!$row) {
|
||||
return '';
|
||||
}
|
||||
$thumb = trim((string)($row['thumb'] ?? ''));
|
||||
if ($thumb !== '' && str_starts_with($thumb, 'data:image/')) {
|
||||
return $thumb;
|
||||
}
|
||||
$path = self::filePath($id);
|
||||
if ($path === null) {
|
||||
return '';
|
||||
}
|
||||
$json = @file_get_contents($path);
|
||||
if ($json === false || $json === '') {
|
||||
return '';
|
||||
}
|
||||
$meta = self::parseSoonMeta($json);
|
||||
$thumb = $meta['thumb'];
|
||||
if ($thumb === '') {
|
||||
return '';
|
||||
}
|
||||
Db::pdo()->prepare('UPDATE soon_templates SET thumb = :th, updated_at = :ua WHERE id = :id')
|
||||
->execute(['th' => $thumb, 'ua' => date('Y-m-d H:i:s'), 'id' => $id]);
|
||||
return $thumb;
|
||||
}
|
||||
|
||||
/** @param array<string, mixed> $data */
|
||||
private static function extractThumb(array $data): string
|
||||
{
|
||||
if (empty($data['frontDisplayPic']) || !is_string($data['frontDisplayPic'])) {
|
||||
return '';
|
||||
}
|
||||
$candidate = trim($data['frontDisplayPic']);
|
||||
if (!str_starts_with($candidate, 'data:image/')) {
|
||||
return '';
|
||||
}
|
||||
if (strlen($candidate) <= self::THUMB_MAX_BYTES) {
|
||||
return $candidate;
|
||||
}
|
||||
return self::compressDataUrl($candidate);
|
||||
}
|
||||
|
||||
private static function compressDataUrl(string $dataUrl): string
|
||||
{
|
||||
if (!preg_match('#^data:(image/[a-zA-Z0-9.+-]+);base64,(.+)$#s', $dataUrl, $m)) {
|
||||
return '';
|
||||
}
|
||||
$bin = base64_decode($m[2], true);
|
||||
if ($bin === false || $bin === '') {
|
||||
return '';
|
||||
}
|
||||
if (!function_exists('imagecreatefromstring')) {
|
||||
return '';
|
||||
}
|
||||
$img = @imagecreatefromstring($bin);
|
||||
if ($img === false) {
|
||||
return '';
|
||||
}
|
||||
$w = imagesx($img);
|
||||
$h = imagesy($img);
|
||||
if ($w < 1 || $h < 1) {
|
||||
imagedestroy($img);
|
||||
return '';
|
||||
}
|
||||
$maxW = 360;
|
||||
if ($w > $maxW) {
|
||||
$newH = max(1, (int)round($h * ($maxW / $w)));
|
||||
$scaled = imagescale($img, $maxW, $newH);
|
||||
if ($scaled !== false) {
|
||||
imagedestroy($img);
|
||||
$img = $scaled;
|
||||
}
|
||||
}
|
||||
ob_start();
|
||||
imagejpeg($img, null, 82);
|
||||
imagedestroy($img);
|
||||
$jpeg = ob_get_clean();
|
||||
if ($jpeg === false || $jpeg === '') {
|
||||
return '';
|
||||
}
|
||||
$out = 'data:image/jpeg;base64,' . base64_encode($jpeg);
|
||||
if (strlen($out) > self::THUMB_MAX_BYTES) {
|
||||
return '';
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
return ['type' => $type, 'thumb' => $thumb];
|
||||
private static function emitThumbBinary(string $thumb, string $rev): void
|
||||
{
|
||||
if (!preg_match('#^data:(image/[a-zA-Z0-9.+-]+);base64,(.+)$#s', $thumb, $m)) {
|
||||
http_response_code(404);
|
||||
exit;
|
||||
}
|
||||
$bin = base64_decode($m[2], true);
|
||||
if ($bin === false) {
|
||||
http_response_code(404);
|
||||
exit;
|
||||
}
|
||||
header('Content-Type: ' . $m[1]);
|
||||
header('Cache-Control: public, max-age=300, must-revalidate');
|
||||
header('ETag: "' . md5($rev . ':' . strlen($bin)) . '"');
|
||||
header('Content-Length: ' . strlen($bin));
|
||||
echo $bin;
|
||||
exit;
|
||||
}
|
||||
|
||||
public static function create(
|
||||
@@ -186,6 +305,8 @@ final class TemplateService
|
||||
$thumb = $meta['thumb'];
|
||||
$size = strlen($json);
|
||||
self::writeFile($id, $json);
|
||||
} elseif ($thumb === '') {
|
||||
$thumb = self::ensureThumbStored($id);
|
||||
}
|
||||
|
||||
Db::pdo()->prepare(
|
||||
@@ -232,6 +353,33 @@ final class TemplateService
|
||||
}
|
||||
}
|
||||
|
||||
/** @return array<string, mixed> */
|
||||
public static function fetchPublicJson(int $id): array
|
||||
{
|
||||
$row = self::find($id);
|
||||
if (!$row || (int)$row['is_active'] !== 1) {
|
||||
Json::fail('not_found', '模板不存在', 404);
|
||||
}
|
||||
$path = self::filePath($id);
|
||||
if ($path === null) {
|
||||
Json::fail('not_found', '模板文件缺失', 404);
|
||||
}
|
||||
$json = @file_get_contents($path);
|
||||
if ($json === false || trim($json) === '') {
|
||||
Json::fail('server_error', '模板内容读取失败', 500);
|
||||
}
|
||||
if (json_decode($json, true) === null && json_last_error() !== JSON_ERROR_NONE) {
|
||||
Json::fail('server_error', '模板 JSON 无效', 500);
|
||||
}
|
||||
return [
|
||||
'id' => $id,
|
||||
'name' => (string)$row['name'],
|
||||
'type' => (int)$row['type'],
|
||||
'updated_at' => (string)$row['updated_at'],
|
||||
'json' => $json,
|
||||
];
|
||||
}
|
||||
|
||||
public static function outputThumb(int $id): void
|
||||
{
|
||||
$row = self::find($id);
|
||||
@@ -239,25 +387,35 @@ final class TemplateService
|
||||
http_response_code(404);
|
||||
exit;
|
||||
}
|
||||
$thumb = (string)($row['thumb'] ?? '');
|
||||
$thumb = trim((string)($row['thumb'] ?? ''));
|
||||
if ($thumb === '' || !str_starts_with($thumb, 'data:image/')) {
|
||||
$thumb = self::ensureThumbStored($id);
|
||||
}
|
||||
if ($thumb === '') {
|
||||
http_response_code(404);
|
||||
exit;
|
||||
}
|
||||
if (!preg_match('#^data:(image/[a-zA-Z0-9.+-]+);base64,(.+)$#', $thumb, $m)) {
|
||||
$rev = (string)$row['updated_at'];
|
||||
self::emitThumbBinary($thumb, $rev);
|
||||
}
|
||||
|
||||
public static function outputThumbAdmin(int $id): void
|
||||
{
|
||||
$row = self::find($id);
|
||||
if (!$row) {
|
||||
http_response_code(404);
|
||||
exit;
|
||||
}
|
||||
$bin = base64_decode($m[2], true);
|
||||
if ($bin === false) {
|
||||
$thumb = trim((string)($row['thumb'] ?? ''));
|
||||
if ($thumb === '' || !str_starts_with($thumb, 'data:image/')) {
|
||||
$thumb = self::ensureThumbStored($id);
|
||||
$row = self::find($id) ?: $row;
|
||||
}
|
||||
if ($thumb === '') {
|
||||
http_response_code(404);
|
||||
exit;
|
||||
}
|
||||
header('Content-Type: ' . $m[1]);
|
||||
header('Cache-Control: public, max-age=86400');
|
||||
header('Content-Length: ' . strlen($bin));
|
||||
echo $bin;
|
||||
exit;
|
||||
self::emitThumbBinary($thumb, (string)$row['updated_at']);
|
||||
}
|
||||
|
||||
public static function outputFile(int $id): void
|
||||
@@ -276,7 +434,8 @@ final class TemplateService
|
||||
}
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
header('Content-Disposition: inline; filename="' . str_replace('"', '', $name) . '.soon"');
|
||||
header('Cache-Control: public, max-age=300');
|
||||
header('Cache-Control: public, max-age=60, must-revalidate');
|
||||
header('ETag: "' . md5((string)$row['updated_at'] . ':' . (int)$row['file_size']) . '"');
|
||||
readfile($path);
|
||||
exit;
|
||||
}
|
||||
|
||||
+36
-3
@@ -162,13 +162,45 @@ GET /api/v1/pay/orders?page=1&size=8
|
||||
| 路径 | 形态 |
|
||||
|------|------|
|
||||
| `GET /api/v1/plans` | `{ items: [...] }` 全量活跃套餐 |
|
||||
| `GET /api/v1/plans/me` | 会员信息 + `recent_orders`(默认 8 条,非翻页) |
|
||||
| `GET /api/v1/auth/me` | 当前用户 + 会员摘要(`is_member` / `tier` / `name`) |
|
||||
| `GET /api/v1/plans/me` | 完整会员配额信息(云端用量等,按需调用) |
|
||||
| `GET /api/v1/settings` | 扁平 key-value 对象,非 `items` 数组 |
|
||||
| `GET /api/v1/templates` | 首页模板列表(仅 `id/name/type`;缩略图与文件按需拉取) |
|
||||
| `GET /api/v1/templates` | 首页模板列表(仅 `id/name/type/updated_at`;缩略图按需拉取) |
|
||||
| `GET /api/v1/templates/{id}` | 模板 JSON 数据(门户标准读取端点,见下) |
|
||||
| `GET /api/v1/templates/{id}/thumb` | 模板缩略图(从 `.soon` 内 `frontDisplayPic` 提取) |
|
||||
| `GET /api/v1/templates/{id}/file` | 完整 `.soon` JSON(点击使用时再下载) |
|
||||
| `GET /api/v1/templates/{id}/file` | **legacy**:裸 `.soon` 流;门户禁止调用,仅供兼容 |
|
||||
| `GET /api/v1/soon-models` | 兼容别名,同 `GET /api/v1/templates` |
|
||||
|
||||
#### `GET /api/v1/templates/{id}`(门户读取模板数据)
|
||||
|
||||
与 `GET /api/v1/files/{id}` 对齐,返回 JSON 包装,**非**裸文件流:
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"data": {
|
||||
"id": 1,
|
||||
"name": "示例模版",
|
||||
"type": 1,
|
||||
"updated_at": "2026-06-09 00:23:29",
|
||||
"json": "{...}"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- `data.json` 为 **字符串**(与 `files/{id}` 一致),前端 `JSON.parse(data.json)` 后加载画布
|
||||
- 公开接口,无需登录
|
||||
|
||||
### Web 虚拟 key 前缀(门户设计页)
|
||||
|
||||
| 前缀 | 含义 | readJsonFile | 保存 writeFile |
|
||||
|------|------|--------------|----------------|
|
||||
| `soondesign_file:{id}:v{ver}` | 云端已登记文件 | `GET /files/{id}` | `PUT /files/{id}` |
|
||||
| `soondesign_template:{id}` | 云端模板(只读源) | `GET /templates/{id}` | `POST /files`(首次保存新建) |
|
||||
| `soondesign_session:...` | 本地/临时会话 | sessionStorage | 已登录 `POST /files`;未登录写 session |
|
||||
|
||||
- **保存/打开**走 JSON API;**仅**首页「下载」走 `GET /files/{id}/download` 落盘 `.soon`
|
||||
|
||||
---
|
||||
|
||||
## 前端约定
|
||||
@@ -203,3 +235,4 @@ GET /api/v1/pay/orders?page=1&size=8
|
||||
| 日期 | 说明 |
|
||||
|------|------|
|
||||
| 2026-06-08 | 初版:统一 page/size/total;files 兼容 limit/offset;pay/orders 分页 |
|
||||
| 2026-06-08 | 新增 GET /templates/{id} JSON 契约;Web 虚拟 key 前缀;/file 标 legacy |
|
||||
|
||||
@@ -13,10 +13,10 @@ img {
|
||||
padding: 0 !important;
|
||||
height: 100%;
|
||||
}
|
||||
.col-left{
|
||||
body:not(.soon-design-page) .col-left{
|
||||
margin-right: 320px;
|
||||
}
|
||||
.col-right{
|
||||
body:not(.soon-design-page) .col-right{
|
||||
float: right;
|
||||
height: 100%;
|
||||
width: 320px;
|
||||
@@ -419,6 +419,13 @@ textarea:hover{
|
||||
}
|
||||
|
||||
/* ── Web 设计页:design1 / design2 统一视口布局(无页面滚动) ── */
|
||||
html:has(body.soon-design-page),
|
||||
body.soon-design-page,
|
||||
body.soon-design-page .layui-fluid.main,
|
||||
body.soon-design-page .layui-row {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
html:has(body.soon-design-page) {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
@@ -437,8 +444,9 @@ body.soon-design-page {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
body.soon-design-page .soon-design-portal-bar,
|
||||
body.soon-design-page .soon-portal-topbar {
|
||||
position: static;
|
||||
position: static !important;
|
||||
width: 100%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
@@ -453,7 +461,7 @@ body.soon-design-page .layui-fluid.main {
|
||||
height: auto !important;
|
||||
overflow: hidden;
|
||||
padding: 0 !important;
|
||||
display: flex;
|
||||
display: flex !important;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
@@ -706,10 +714,19 @@ body.soon-design-page .guide-line-drag {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
body.soon-design-page #source_front,
|
||||
body.soon-design-page #source_front2,
|
||||
body.soon-design-page #source_back {
|
||||
body.soon-design-page.soon-design--type1 #source_front,
|
||||
body.soon-design-page.soon-design--type1 #source_front2,
|
||||
body.soon-design-page.soon-design--type1 #source_back {
|
||||
width: 2787px;
|
||||
height: 2787px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
body.soon-design-page.soon-design--type2 #source_front,
|
||||
body.soon-design-page.soon-design--type2 #source_front2,
|
||||
body.soon-design-page.soon-design--type2 #source_back {
|
||||
width: auto;
|
||||
height: auto;
|
||||
max-width: none;
|
||||
border-radius: 0;
|
||||
}
|
||||
@@ -20,6 +20,31 @@
|
||||
color: var(--soon-text) !important;
|
||||
}
|
||||
|
||||
/* layui 关闭/状态图标为深色 sprite,深底上需反色 */
|
||||
.layui-layer.soon-layer .layui-layer-setwin .layui-layer-ico,
|
||||
.layui-layer-soon-layer .layui-layer-setwin .layui-layer-ico,
|
||||
.layui-layer.soon-layer .layui-layer-ico.layui-layer-close,
|
||||
.layui-layer-soon-layer .layui-layer-ico.layui-layer-close {
|
||||
filter: brightness(0) invert(1);
|
||||
opacity: 0.88;
|
||||
}
|
||||
|
||||
.layui-layer.soon-layer .layui-layer-setwin .layui-layer-ico:hover,
|
||||
.layui-layer-soon-layer .layui-layer-setwin .layui-layer-ico:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.layui-layer.soon-layer.layui-layer-dialog .layui-layer-content .layui-layer-ico,
|
||||
.layui-layer-soon-layer.layui-layer-dialog .layui-layer-content .layui-layer-ico {
|
||||
filter: brightness(0) invert(1);
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.layui-layer.soon-layer .layui-layer-content font,
|
||||
.layui-layer-soon-layer .layui-layer-content font {
|
||||
color: inherit !important;
|
||||
}
|
||||
|
||||
.layui-layer.soon-layer.soon-layer--pay .layui-layer-content,
|
||||
.layui-layer-soon-layer.soon-layer--pay .layui-layer-content {
|
||||
overflow: visible !important;
|
||||
@@ -104,13 +129,33 @@
|
||||
}
|
||||
|
||||
.layui-layer-dialog .layui-layer-content {
|
||||
background-color: var(--soon-bg-panel);
|
||||
color: var(--soon-text);
|
||||
background-color: var(--soon-bg-panel) !important;
|
||||
color: var(--soon-text) !important;
|
||||
}
|
||||
|
||||
.layui-layer-dialog .layui-layer-title {
|
||||
background-color: var(--soon-bg-elevated);
|
||||
color: var(--soon-text-strong);
|
||||
border-bottom: 1px solid var(--soon-border-subtle);
|
||||
background-color: var(--soon-bg-elevated) !important;
|
||||
color: var(--soon-text-strong) !important;
|
||||
border-bottom: 1px solid var(--soon-border-subtle) !important;
|
||||
}
|
||||
|
||||
body.soon-design-page .layui-layer .layui-layer-title {
|
||||
background-color: var(--soon-bg-elevated) !important;
|
||||
color: var(--soon-text-strong) !important;
|
||||
border-bottom: 1px solid var(--soon-border-subtle) !important;
|
||||
}
|
||||
|
||||
body.soon-design-page .layui-layer .layui-layer-content {
|
||||
color: var(--soon-text) !important;
|
||||
}
|
||||
|
||||
body.soon-design-page .layui-layer .layui-layer-setwin .layui-layer-ico {
|
||||
filter: brightness(0) invert(1);
|
||||
opacity: 0.88;
|
||||
}
|
||||
|
||||
body.soon-design-page .layui-layer .layui-layer-setwin .layui-layer-ico:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.layui-layer.soon-toast,
|
||||
@@ -188,22 +233,3 @@
|
||||
color: var(--soon-text-strong) !important;
|
||||
border-radius: var(--soon-radius-sm) !important;
|
||||
}
|
||||
|
||||
.soon-design-portal-bar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 60px;
|
||||
z-index: 10000;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
body.soon-design-page {
|
||||
padding-top: 60px !important;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body.soon-design-page .layui-fluid {
|
||||
height: calc(100% - 60px) !important;
|
||||
}
|
||||
|
||||
@@ -2,9 +2,23 @@
|
||||
'use strict';
|
||||
|
||||
var FILE_PREFIX = 'soondesign_file:';
|
||||
var TEMPLATE_PREFIX = 'soondesign_template:';
|
||||
|
||||
function soonParseTemplateKey(key) {
|
||||
if (!key || typeof key !== 'string') return null;
|
||||
var m = key.match(/^soondesign_template:(\d+)$/);
|
||||
if (!m) return null;
|
||||
return { id: parseInt(m[1], 10) };
|
||||
}
|
||||
|
||||
function soonMakeTemplateKey(id) {
|
||||
return TEMPLATE_PREFIX + id;
|
||||
}
|
||||
|
||||
function soonIsWebPortal() {
|
||||
return typeof window !== 'undefined' && window.platformBridge && !window.fs;
|
||||
if (typeof window === 'undefined') return false;
|
||||
if (window.SOON_DEPLOY_CONFIG) return true;
|
||||
return !!(window.platformBridge && window.fs == null);
|
||||
}
|
||||
|
||||
function soonGetAccessToken() {
|
||||
@@ -31,6 +45,10 @@
|
||||
var meta = window._soonFileMeta;
|
||||
return (meta && meta.name) ? meta.name : 'design.soon';
|
||||
}
|
||||
if (n.indexOf(TEMPLATE_PREFIX) === 0) {
|
||||
var tpl = soonParseTemplateKey(n);
|
||||
return tpl ? ('template-' + tpl.id + '.soon') : 'template.soon';
|
||||
}
|
||||
if (n.indexOf('soondesign_session:') === 0) {
|
||||
n = n.replace(/^soondesign_session:/, '');
|
||||
}
|
||||
@@ -45,8 +63,68 @@
|
||||
return soonMakeFileKey(data.id, data.version);
|
||||
}
|
||||
|
||||
function soonSyncOpenNavigation(fileKey, type) {
|
||||
if (typeof sessionStorage === 'undefined') {
|
||||
if (!fileKey || fileKey.indexOf(FILE_PREFIX) !== 0) window._soonFileMeta = null;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (fileKey) {
|
||||
sessionStorage.setItem('soondesign_open_file', fileKey);
|
||||
sessionStorage.setItem('soondesign_open_type', String(type || 1));
|
||||
if (fileKey.indexOf(FILE_PREFIX) === 0 && window._soonFileMeta) {
|
||||
sessionStorage.setItem('soondesign_open_meta', JSON.stringify(window._soonFileMeta));
|
||||
} else {
|
||||
sessionStorage.removeItem('soondesign_open_meta');
|
||||
if (fileKey.indexOf(FILE_PREFIX) !== 0) window._soonFileMeta = null;
|
||||
}
|
||||
} else {
|
||||
sessionStorage.removeItem('soondesign_open_file');
|
||||
sessionStorage.removeItem('soondesign_open_type');
|
||||
sessionStorage.removeItem('soondesign_open_meta');
|
||||
window._soonFileMeta = null;
|
||||
}
|
||||
} catch (e) { /* ignore quota */ }
|
||||
}
|
||||
|
||||
function soonResolveOpenFileKey(getParams) {
|
||||
var params = typeof getParams === 'function' ? getParams() : null;
|
||||
if (!params || typeof params.has !== 'function') return '';
|
||||
var file = params.has('file') ? (params.get('file') || '') : null;
|
||||
if (file === null && typeof sessionStorage !== 'undefined') {
|
||||
try {
|
||||
file = sessionStorage.getItem('soondesign_open_file') || '';
|
||||
if (file) {
|
||||
sessionStorage.removeItem('soondesign_open_file');
|
||||
sessionStorage.removeItem('soondesign_open_type');
|
||||
}
|
||||
} catch (e) {
|
||||
file = '';
|
||||
}
|
||||
}
|
||||
return file || '';
|
||||
}
|
||||
|
||||
function soonConsumeOpenMeta(fileKey) {
|
||||
if (!fileKey || fileKey.indexOf(FILE_PREFIX) !== 0) {
|
||||
window._soonFileMeta = null;
|
||||
return;
|
||||
}
|
||||
if (typeof sessionStorage === 'undefined') return;
|
||||
try {
|
||||
var metaRaw = sessionStorage.getItem('soondesign_open_meta');
|
||||
if (metaRaw) {
|
||||
window._soonFileMeta = JSON.parse(metaRaw);
|
||||
sessionStorage.removeItem('soondesign_open_meta');
|
||||
}
|
||||
} catch (e) { /* ignore */ }
|
||||
}
|
||||
|
||||
function soonBindFileMeta(fileKey, opts) {
|
||||
if (!fileKey || fileKey.indexOf(FILE_PREFIX) !== 0) return;
|
||||
if (!fileKey || fileKey.indexOf(FILE_PREFIX) !== 0) {
|
||||
window._soonFileMeta = null;
|
||||
return;
|
||||
}
|
||||
var parsed = soonParseFileKey(fileKey);
|
||||
if (!parsed) return;
|
||||
var prev = window._soonFileMeta || {};
|
||||
@@ -137,16 +215,22 @@
|
||||
soonApplyMembership(null);
|
||||
return Promise.resolve(_membership);
|
||||
}
|
||||
var url = soonApiBase() + '/plans/me';
|
||||
var url = soonApiBase() + '/auth/me';
|
||||
var fetchFn = typeof window.soonAuthedFetch === 'function'
|
||||
? window.soonAuthedFetch(url, { headers: { Accept: 'application/json' } })
|
||||
: fetch(url, { headers: { Authorization: 'Bearer ' + soonGetAccessToken(), Accept: 'application/json' } });
|
||||
_membership.loading = fetchFn
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (r) {
|
||||
if (r.status === 401) {
|
||||
soonApplyMembership(null);
|
||||
return null;
|
||||
}
|
||||
return r.json();
|
||||
})
|
||||
.then(function (j) {
|
||||
if (j && j.ok && j.data) {
|
||||
soonApplyMembership(j.data.membership || j.data);
|
||||
} else {
|
||||
soonApplyMembership(j.data);
|
||||
} else if (j !== null) {
|
||||
soonApplyMembership(null);
|
||||
}
|
||||
return _membership;
|
||||
@@ -184,20 +268,27 @@
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (soonIsMember()) {
|
||||
onAllowed();
|
||||
function allowOrActivate() {
|
||||
if (soonIsMember()) {
|
||||
onAllowed();
|
||||
return;
|
||||
}
|
||||
if (typeof window.soonShowActivateGate === 'function') {
|
||||
window.soonShowActivateGate({
|
||||
reason: actionLabel || '导出或打印',
|
||||
onSuccess: function () {
|
||||
onAllowed();
|
||||
},
|
||||
});
|
||||
} else {
|
||||
soonToast('请先激活会员后再' + (actionLabel || '操作'), 'warn');
|
||||
}
|
||||
}
|
||||
if (!_membership.loaded) {
|
||||
soonLoadMembership(false).then(allowOrActivate);
|
||||
return;
|
||||
}
|
||||
if (typeof window.soonShowActivateGate === 'function') {
|
||||
window.soonShowActivateGate({
|
||||
reason: actionLabel || '导出或打印',
|
||||
onSuccess: function () {
|
||||
onAllowed();
|
||||
},
|
||||
});
|
||||
} else {
|
||||
soonToast('请先激活会员后再' + (actionLabel || '操作'), 'warn');
|
||||
}
|
||||
allowOrActivate();
|
||||
}
|
||||
|
||||
function soonGuardCloudSave() {
|
||||
@@ -223,7 +314,11 @@
|
||||
|
||||
function soonOpenSoonJsonLocally(j, fileName) {
|
||||
var key = soonPutSoonSession(j, fileName);
|
||||
if (!key) return '';
|
||||
if (!key) {
|
||||
soonToast('无法打开文件(存储空间不足)', 'error');
|
||||
return '';
|
||||
}
|
||||
soonScheduleCloudImport(key, fileName);
|
||||
var type = soonSoonTypeFromJson(j);
|
||||
if (window.platformBridge && window.platformBridge.openDesignPage) {
|
||||
window.platformBridge.openDesignPage(key, type);
|
||||
@@ -231,6 +326,75 @@
|
||||
return key;
|
||||
}
|
||||
|
||||
var PENDING_IMPORT_KEY = 'soondesign_pending_import';
|
||||
|
||||
function soonScheduleCloudImport(sessionKey, fileName) {
|
||||
if (!soonGetAccessToken() || !sessionKey) return;
|
||||
if (sessionKey.indexOf('soondesign_session:') !== 0) return;
|
||||
try {
|
||||
sessionStorage.setItem(PENDING_IMPORT_KEY, JSON.stringify({
|
||||
sessionKey: sessionKey,
|
||||
name: soonNormalizeSoonName(fileName || 'design.soon'),
|
||||
at: Date.now()
|
||||
}));
|
||||
} catch (e) { /* ignore */ }
|
||||
}
|
||||
|
||||
function soonClearPendingImport() {
|
||||
try { sessionStorage.removeItem(PENDING_IMPORT_KEY); } catch (e) { /* ignore */ }
|
||||
}
|
||||
|
||||
function soonTryConsumeCloudImport(currentFileKey) {
|
||||
if (!currentFileKey || currentFileKey.indexOf('soondesign_session:') !== 0) return;
|
||||
if (!soonGetAccessToken()) return;
|
||||
if (typeof window.openAs !== 'undefined' && window.openAs && window.openAs.name &&
|
||||
window.openAs.name.indexOf(FILE_PREFIX) === 0) {
|
||||
soonClearPendingImport();
|
||||
return;
|
||||
}
|
||||
var pendingRaw;
|
||||
try {
|
||||
pendingRaw = sessionStorage.getItem(PENDING_IMPORT_KEY);
|
||||
} catch (e) {
|
||||
return;
|
||||
}
|
||||
if (!pendingRaw) return;
|
||||
var pending;
|
||||
try {
|
||||
pending = JSON.parse(pendingRaw);
|
||||
} catch (e) {
|
||||
try { sessionStorage.removeItem(PENDING_IMPORT_KEY); } catch (e2) { /* ignore */ }
|
||||
return;
|
||||
}
|
||||
if (!pending || pending.sessionKey !== currentFileKey) return;
|
||||
try { sessionStorage.removeItem(PENDING_IMPORT_KEY); } catch (e) { /* ignore */ }
|
||||
|
||||
var jsonRaw;
|
||||
try {
|
||||
jsonRaw = sessionStorage.getItem(currentFileKey);
|
||||
if (!jsonRaw && typeof localStorage !== 'undefined') jsonRaw = localStorage.getItem(currentFileKey);
|
||||
} catch (e) {
|
||||
return;
|
||||
}
|
||||
if (!jsonRaw) return;
|
||||
|
||||
var bridge = window.platformBridge;
|
||||
if (!bridge || typeof bridge.importSoonFile !== 'function') return;
|
||||
|
||||
bridge.importSoonFile(pending.name || 'design.soon', jsonRaw).then(function (res) {
|
||||
if (res && res.fileKey && typeof window.openAs !== 'undefined' && window.openAs) {
|
||||
window.openAs.name = res.fileKey;
|
||||
}
|
||||
}).catch(function (err) {
|
||||
if (err && err.status) return;
|
||||
soonToast((err && err.message) ? err.message : '云端登记失败,可稍后保存重试', 'warn');
|
||||
});
|
||||
}
|
||||
|
||||
function soonReportOpenLoadError(message) {
|
||||
soonToast(message || '文件加载失败', 'error');
|
||||
}
|
||||
|
||||
function soonToast(message, type) {
|
||||
if (typeof window.soonToast === 'function') window.soonToast(message, type);
|
||||
}
|
||||
@@ -289,6 +453,14 @@
|
||||
|
||||
function soonDisplayFileName(pathOrKey) {
|
||||
if (!pathOrKey) return '';
|
||||
if (pathOrKey.indexOf(TEMPLATE_PREFIX) === 0) {
|
||||
var tplMeta = window._soonTemplateMeta;
|
||||
var tplParsed = soonParseTemplateKey(pathOrKey);
|
||||
if (tplMeta && tplParsed && tplMeta.id === tplParsed.id && tplMeta.name) {
|
||||
return tplMeta.name;
|
||||
}
|
||||
return tplParsed ? ('模板 #' + tplParsed.id) : pathOrKey;
|
||||
}
|
||||
if (pathOrKey.indexOf(FILE_PREFIX) === 0) {
|
||||
var meta = window._soonFileMeta;
|
||||
if (meta && meta.name) return meta.name;
|
||||
@@ -333,12 +505,21 @@
|
||||
window.soonNormalizeSoonName = soonNormalizeSoonName;
|
||||
window.soonApplyCloudMeta = soonApplyCloudMeta;
|
||||
window.soonBindFileMeta = soonBindFileMeta;
|
||||
window.soonSyncOpenNavigation = soonSyncOpenNavigation;
|
||||
window.soonResolveOpenFileKey = soonResolveOpenFileKey;
|
||||
window.soonConsumeOpenMeta = soonConsumeOpenMeta;
|
||||
window.soonFormatOpenTitle = soonFormatOpenTitle;
|
||||
window.soonGuardCloudSave = soonGuardCloudSave;
|
||||
window.soonGuardPreviewDeliver = soonGuardPreviewDeliver;
|
||||
window.soonHandlePayReturn = soonHandlePayReturn;
|
||||
window.soonOpenSoonJsonLocally = soonOpenSoonJsonLocally;
|
||||
window.soonPutSoonSession = soonPutSoonSession;
|
||||
window.soonScheduleCloudImport = soonScheduleCloudImport;
|
||||
window.soonTryConsumeCloudImport = soonTryConsumeCloudImport;
|
||||
window.soonClearPendingImport = soonClearPendingImport;
|
||||
window.soonReportOpenLoadError = soonReportOpenLoadError;
|
||||
window.soonParseTemplateKey = soonParseTemplateKey;
|
||||
window.soonMakeTemplateKey = soonMakeTemplateKey;
|
||||
window.soonSoonTypeFromJson = soonSoonTypeFromJson;
|
||||
window.soonLoadMembership = soonLoadMembership;
|
||||
window.soonApplyMembership = soonApplyMembership;
|
||||
|
||||
@@ -260,7 +260,7 @@
|
||||
return '未获取到支付信息';
|
||||
}
|
||||
|
||||
function payEmptyStateHtml(channel) {
|
||||
function paySheetHtml(opts) {
|
||||
opts = opts || {};
|
||||
var resumeOrderNo = opts.orderNo || null;
|
||||
var channels = opts.displayChannels || _displayChannels;
|
||||
|
||||
@@ -6,7 +6,9 @@ $("#object_attribute").css("height", "470px");
|
||||
$("#obj_list").css("height", document.body.clientHeight - 520);
|
||||
$(".obj_list").css("height", $("#obj_list").height() - 32);
|
||||
|
||||
$(".main").show();
|
||||
var mainEl = document.querySelector('.layui-fluid.main');
|
||||
if (mainEl) mainEl.style.display = 'flex';
|
||||
else $(".main").show();
|
||||
$(".canvas_box").css("height", document.body.clientHeight - 100);
|
||||
zoom = $(".canvas_bg_img").width() / 2787
|
||||
$("#canvas1").attr("width", $(".canvas_box").width() / zoom);
|
||||
@@ -259,7 +261,7 @@ if (typeof ipcRenderer !== 'undefined' && ipcRenderer) {
|
||||
applySysLan(window.platformBridge.getLocale());
|
||||
}
|
||||
function onCloseConfirm() {
|
||||
layer.confirm('<font style="color:black">' + language_str("whetherSave") + '</font>', {
|
||||
layer.confirm(language_str("whetherSave"), {
|
||||
btn: [language_str("save"), language_str("noSave"), language_str("cancel")]
|
||||
, btn3: function (index, layero) {}
|
||||
}, function (index, layero) {
|
||||
@@ -418,28 +420,14 @@ function addBackground() {
|
||||
recordObjs2.push(JSON.stringify(objs2));
|
||||
recordJson2.push(canvas2.toJSON(TO_JSON_PROPERTIES));
|
||||
|
||||
let file = GetFile().get('file');
|
||||
if (!file && typeof sessionStorage !== 'undefined') {
|
||||
try {
|
||||
file = sessionStorage.getItem('soondesign_open_file');
|
||||
if (file) {
|
||||
sessionStorage.removeItem('soondesign_open_file');
|
||||
sessionStorage.removeItem('soondesign_open_type');
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
var file = typeof window.soonResolveOpenFileKey === 'function'
|
||||
? window.soonResolveOpenFileKey(GetFile)
|
||||
: (GetFile().get('file') || '');
|
||||
if (file && file != 'empty') {
|
||||
if (file.indexOf('soondesign_file:') === 0 && typeof sessionStorage !== 'undefined') {
|
||||
try {
|
||||
var metaRaw = sessionStorage.getItem('soondesign_open_meta');
|
||||
if (metaRaw) {
|
||||
window._soonFileMeta = JSON.parse(metaRaw);
|
||||
sessionStorage.removeItem('soondesign_open_meta');
|
||||
}
|
||||
} catch (e) {}
|
||||
if (typeof window.soonConsumeOpenMeta === 'function') {
|
||||
window.soonConsumeOpenMeta(file);
|
||||
}
|
||||
var p = window.openFile(file)
|
||||
if (p && p.then) p.then(function() {}, function() {})
|
||||
window.openFile(file);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -401,6 +401,7 @@ window.display_func = function display_func(img1, img2, img3) {
|
||||
|
||||
layer.open({
|
||||
type: 1,
|
||||
skin: 'soon-layer',
|
||||
area: [w, h],
|
||||
title: language_str("display"), //不显示标题栏"预览"
|
||||
shadeClose: true, //点击遮罩关闭
|
||||
@@ -1097,8 +1098,14 @@ window.openFile = function open(file, jAlready) {
|
||||
if (!file) return;
|
||||
if (typeof window.soonBindFileMeta === 'function') window.soonBindFileMeta(file);
|
||||
function doOpenWithJson(j) {
|
||||
if (!j || !background_image) return;
|
||||
if (!j.f || !j.f.objects || !Array.isArray(j.f.objects) || j.f.objects.length === 0) return;
|
||||
if (!j || !background_image) {
|
||||
if (typeof window.soonReportOpenLoadError === 'function') window.soonReportOpenLoadError('模板/文件加载失败');
|
||||
return;
|
||||
}
|
||||
if (!j.f || !j.f.objects || !Array.isArray(j.f.objects) || j.f.objects.length === 0) {
|
||||
if (typeof window.soonReportOpenLoadError === 'function') window.soonReportOpenLoadError('模板/文件加载失败');
|
||||
return;
|
||||
}
|
||||
if (!j.b || !j.b.objects || !Array.isArray(j.b.objects) || j.b.objects.length === 0) {
|
||||
j.b = { version: '4.6.0', objects: [], hoverCursor: 'move' };
|
||||
}
|
||||
@@ -1173,12 +1180,14 @@ window.openFile = function open(file, jAlready) {
|
||||
var j2 = canvas2.toJSON(props);
|
||||
recordJson2.push(j2);
|
||||
}, 0);
|
||||
if (typeof window.soonTryConsumeCloudImport === 'function') window.soonTryConsumeCloudImport(file);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (arguments.length >= 2 && jAlready) {
|
||||
doOpenWithJson(jAlready);
|
||||
openAs.name = file;
|
||||
if (typeof window.soonTryConsumeCloudImport === 'function') window.soonTryConsumeCloudImport(file);
|
||||
return;
|
||||
}
|
||||
if (window.platformBridge && window.platformBridge.readJsonFile) {
|
||||
@@ -1186,8 +1195,12 @@ window.openFile = function open(file, jAlready) {
|
||||
if (j) {
|
||||
doOpenWithJson(j);
|
||||
openAs.name = file;
|
||||
} else if (typeof window.soonReportOpenLoadError === 'function') {
|
||||
window.soonReportOpenLoadError('模板/文件加载失败');
|
||||
}
|
||||
}).catch(function() {});
|
||||
}).catch(function () {
|
||||
if (typeof window.soonReportOpenLoadError === 'function') window.soonReportOpenLoadError('模板/文件加载失败');
|
||||
});
|
||||
}
|
||||
try {
|
||||
var fs = typeof require !== 'undefined' ? require('fs') : null;
|
||||
@@ -1252,6 +1265,9 @@ function cloudSaveSuccessLabel(res, fallback) {
|
||||
|
||||
function onCloudWriteDone(res, fallback, callback) {
|
||||
applyCloudWriteResult(res, fallback);
|
||||
if (res && res.fileKey && typeof window.soonClearPendingImport === 'function') {
|
||||
window.soonClearPendingImport();
|
||||
}
|
||||
layer.msg(language_str("saveSucc") + cloudSaveSuccessLabel(res, fallback));
|
||||
if (callback && typeof callback === 'function') callback();
|
||||
}
|
||||
@@ -1260,6 +1276,15 @@ function guardWebSave() {
|
||||
return typeof window.soonGuardCloudSave === 'function' ? window.soonGuardCloudSave() : true;
|
||||
}
|
||||
|
||||
function reportSaveError(err) {
|
||||
if (err && err.status) return;
|
||||
if (typeof window.soonShowApiError === 'function') {
|
||||
window.soonShowApiError({ status: err && err.status, message: (err && err.message) || '保存失败' });
|
||||
} else if (typeof window.soonToast === 'function') {
|
||||
window.soonToast('保存失败', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function saveAs(op1, callback) {
|
||||
// 临时移除辅助线,保存后再恢复
|
||||
let guideLines1 = [];
|
||||
@@ -1311,7 +1336,7 @@ function saveAs(op1, callback) {
|
||||
if (window.platformBridge && window.platformBridge.writeFile) {
|
||||
window.platformBridge.writeFile(fp, content).then(function (res) {
|
||||
onCloudWriteDone(res, fp, callback);
|
||||
}).catch(function () {});
|
||||
}).catch(reportSaveError);
|
||||
return;
|
||||
}
|
||||
var fs = typeof require !== 'undefined' ? require('fs') : null;
|
||||
@@ -1322,7 +1347,7 @@ function saveAs(op1, callback) {
|
||||
saveHistory();
|
||||
if (callback && typeof callback === 'function') callback();
|
||||
}
|
||||
}).catch(function(err) {});
|
||||
}).catch(reportSaveError);
|
||||
}
|
||||
|
||||
function save(op1, callback) {
|
||||
@@ -1365,7 +1390,7 @@ function save(op1, callback) {
|
||||
if (window.platformBridge && window.platformBridge.writeFile) {
|
||||
window.platformBridge.writeFile(openAs.name, content).then(function (res) {
|
||||
onCloudWriteDone(res, openAs.name, callback);
|
||||
}).catch(function () {});
|
||||
}).catch(reportSaveError);
|
||||
return;
|
||||
}
|
||||
var fs = typeof require !== 'undefined' ? require('fs') : null;
|
||||
@@ -1394,7 +1419,7 @@ function save(op1, callback) {
|
||||
if (window.platformBridge && window.platformBridge.writeFile) {
|
||||
window.platformBridge.writeFile(fp, content).then(function (res) {
|
||||
onCloudWriteDone(res, fp, callback);
|
||||
}).catch(function () {});
|
||||
}).catch(reportSaveError);
|
||||
return;
|
||||
}
|
||||
var fs = typeof require !== 'undefined' ? require('fs') : null;
|
||||
@@ -1405,7 +1430,7 @@ function save(op1, callback) {
|
||||
saveHistory();
|
||||
if (callback && typeof callback === 'function') callback();
|
||||
}
|
||||
}).catch(function(err) {});
|
||||
}).catch(reportSaveError);
|
||||
}
|
||||
|
||||
window.saveHistory = function saveHistory() {
|
||||
|
||||
@@ -1345,7 +1345,7 @@ $("#open").click(function () {
|
||||
OpenDialog();
|
||||
return;
|
||||
}
|
||||
layer.confirm('<font style="color:black">' + language_str("whetherSave") + '</font>', function (index) {//confirm 是否保存当前文件�?
|
||||
layer.confirm(language_str("whetherSave"), function (index) {//confirm 是否保存当前文件�?
|
||||
if (typeof window.output === 'function') {
|
||||
window.output(OpenDialog);
|
||||
}
|
||||
@@ -1375,18 +1375,14 @@ $("#open").click(function () {
|
||||
}
|
||||
}
|
||||
function importAndOpen(j, fileName) {
|
||||
var tok = typeof window.soonGetAccessToken === 'function'
|
||||
? window.soonGetAccessToken()
|
||||
: (localStorage.getItem('soon_access') || '');
|
||||
if (!tok && typeof window.soonPutSoonSession === 'function') {
|
||||
if (typeof window.soonPutSoonSession === 'function') {
|
||||
var localKey = window.soonPutSoonSession(j, fileName || 'design.soon');
|
||||
if (localKey) openWithKey(localKey, j);
|
||||
return;
|
||||
if (!localKey) return;
|
||||
if (typeof window.soonScheduleCloudImport === 'function') {
|
||||
window.soonScheduleCloudImport(localKey, fileName || 'design.soon');
|
||||
}
|
||||
openWithKey(localKey, j);
|
||||
}
|
||||
if (!window.platformBridge || !window.platformBridge.importSoonFile) return;
|
||||
window.platformBridge.importSoonFile(fileName || 'design.soon', j).then(function(res) {
|
||||
if (res && res.fileKey) openWithKey(res.fileKey, j);
|
||||
}).catch(function () {});
|
||||
}
|
||||
if (file && file.text) {
|
||||
file.text().then(function(t) {
|
||||
@@ -1445,7 +1441,7 @@ $("#new").click(function () {
|
||||
else if (window.platformBridge) window.platformBridge.openFirstPage();
|
||||
return;
|
||||
}
|
||||
layer.confirm('<font style="color:black">' + language_str("whetherSave") + '</font>', function (index) {//confirm 是否保存当前文件�?
|
||||
layer.confirm(language_str("whetherSave"), function (index) {//confirm 是否保存当前文件�?
|
||||
if (typeof window.output === 'function') {
|
||||
window.output(function() { if (ipcRenderer) ipcRenderer.send('open-first-page'); else if (window.platformBridge) window.platformBridge.openFirstPage(); });
|
||||
}
|
||||
|
||||
@@ -2,15 +2,40 @@
|
||||
// 注意:此代码在layui.use回调函数内部执行
|
||||
// 全局变量已在主入口文件中定义,这里只需要初始化
|
||||
|
||||
$('#canvas1').attr('height', document.body.clientHeight - 100)
|
||||
$('#canvas2').attr('height', document.body.clientHeight - 100)
|
||||
$('#object_attribute').css('height', '470px')
|
||||
$('#obj_list').css('height', document.body.clientHeight - 520)
|
||||
$('.obj_list').css('height', $('#obj_list').height() - 32)
|
||||
function soonDesign2MainHeight() {
|
||||
var main = document.querySelector('.layui-fluid.main');
|
||||
if (main && main.clientHeight > 40) return main.clientHeight;
|
||||
var tb = document.getElementById('portal-topbar');
|
||||
return Math.max(480, window.innerHeight - (tb ? tb.offsetHeight : 0));
|
||||
}
|
||||
|
||||
$('.main').show()
|
||||
$('#canvas1').attr('width', $('#canvas-div').width())
|
||||
$('#canvas2').attr('width', $('#canvas-div').width())
|
||||
function soonDesign2CanvasWidth() {
|
||||
var w = $('#canvas-div').width();
|
||||
if (w > 0) return w;
|
||||
w = $('.col-left').width();
|
||||
if (w > 0) return Math.max(320, w - 70);
|
||||
return Math.max(320, window.innerWidth - 390);
|
||||
}
|
||||
|
||||
function layoutDesign2Shell() {
|
||||
var isWeb = !!(window.SOON_DEPLOY_CONFIG);
|
||||
var mainH = soonDesign2MainHeight();
|
||||
var canvasW = soonDesign2CanvasWidth();
|
||||
var canvasH = isWeb ? Math.max(400, mainH - 130) : (document.body.clientHeight - 100);
|
||||
$('#canvas1').attr('width', canvasW).attr('height', canvasH);
|
||||
$('#canvas2').attr('width', canvasW).attr('height', canvasH);
|
||||
if (!isWeb) {
|
||||
$('#object_attribute').css('height', '470px');
|
||||
$('#obj_list').css('height', Math.max(120, mainH - 520));
|
||||
$('.obj_list').css('height', Math.max(80, $('#obj_list').height() - 32));
|
||||
}
|
||||
return { canvasW: canvasW, canvasH: canvasH };
|
||||
}
|
||||
|
||||
var mainEl = document.querySelector('.layui-fluid.main');
|
||||
if (mainEl) mainEl.style.display = 'flex';
|
||||
else $('.main').show();
|
||||
var _design2Layout = layoutDesign2Shell();
|
||||
|
||||
// 初始化Canvas对象
|
||||
// 优化性能:在创建 Fabric Canvas 之前,先设置 canvas context 的 willReadFrequently
|
||||
@@ -42,11 +67,19 @@ is_bgi_add = false
|
||||
window.ctx1 = ctx1;
|
||||
window.ctx2 = ctx2;
|
||||
window.is_bgi_add = is_bgi_add;
|
||||
canvas1.zoomToPoint(new fabric.Point(canvas1.width / 2, canvas1.height / 2), $('#canvas-div').width() / 1200 > 1 ? 1 : $('#canvas-div').width() / 1200)
|
||||
//console.log(canvas1.width / 2, canvas1.height / 2);
|
||||
//console.log($("#canvas-div").width() / 1200 > 1 ? 1 : $("#canvas-div").width() / 1200);
|
||||
canvas2.zoomToPoint(new fabric.Point(canvas2.width / 2, canvas2.height / 2), $('#canvas-div').width() / 1200 > 1 ? 1 : $('#canvas-div').width() / 1200)
|
||||
zoom = $('#canvas-div').width() / 1200 > 1 ? 1 : $('#canvas-div').width() / 1200
|
||||
canvas1.zoomToPoint(new fabric.Point(canvas1.width / 2, canvas1.height / 2), (function () {
|
||||
var divW = $('#canvas-div').width() || _design2Layout.canvasW || 800;
|
||||
return divW / 1200 > 1 ? 1 : divW / 1200;
|
||||
})())
|
||||
canvas2.zoomToPoint(new fabric.Point(canvas2.width / 2, canvas2.height / 2), (function () {
|
||||
var divW = $('#canvas-div').width() || _design2Layout.canvasW || 800;
|
||||
return divW / 1200 > 1 ? 1 : divW / 1200;
|
||||
})())
|
||||
zoom = (function () {
|
||||
var divW = $('#canvas-div').width() || _design2Layout.canvasW || 800;
|
||||
var z = divW / 1200 > 1 ? 1 : divW / 1200;
|
||||
return z > 0 ? z : 0.5;
|
||||
})()
|
||||
|
||||
// 初始化标尺
|
||||
function initRulers() {
|
||||
@@ -486,6 +519,26 @@ function initRulers() {
|
||||
|
||||
}
|
||||
initRulers();
|
||||
if (window.SOON_DEPLOY_CONFIG && ($('#canvas-div').width() || 0) < 100) {
|
||||
requestAnimationFrame(function () {
|
||||
requestAnimationFrame(function () {
|
||||
var layout = layoutDesign2Shell();
|
||||
var divW = layout.canvasW;
|
||||
zoom = divW / 1200 > 1 ? 1 : divW / 1200;
|
||||
if (zoom <= 0) zoom = 0.5;
|
||||
canvas1.setWidth(layout.canvasW);
|
||||
canvas1.setHeight(layout.canvasH);
|
||||
canvas2.setWidth(layout.canvasW);
|
||||
canvas2.setHeight(layout.canvasH);
|
||||
canvas1.zoomToPoint(new fabric.Point(canvas1.width / 2, canvas1.height / 2), zoom);
|
||||
canvas2.zoomToPoint(new fabric.Point(canvas2.width / 2, canvas2.height / 2), zoom);
|
||||
window.zoom = zoom;
|
||||
canvas1.renderAll();
|
||||
canvas2.renderAll();
|
||||
initRulers();
|
||||
});
|
||||
});
|
||||
}
|
||||
$('#source_front').width(zoom * $('#source_front').width())
|
||||
$('#source_back').width(zoom * $('#source_back').width())
|
||||
$('.canvas-container:eq(1)').hide()
|
||||
@@ -1629,28 +1682,14 @@ function addBackground() {
|
||||
recordObjs2.push(JSON.stringify(objs2))
|
||||
recordJson2.push(canvas2.toJSON(TO_JSON_PROPERTIES))
|
||||
|
||||
let file = GetFile().get('file');
|
||||
if (!file && typeof sessionStorage !== 'undefined') {
|
||||
try {
|
||||
file = sessionStorage.getItem('soondesign_open_file');
|
||||
if (file) {
|
||||
sessionStorage.removeItem('soondesign_open_file');
|
||||
sessionStorage.removeItem('soondesign_open_type');
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
var file = typeof window.soonResolveOpenFileKey === 'function'
|
||||
? window.soonResolveOpenFileKey(GetFile)
|
||||
: (GetFile().get('file') || '');
|
||||
if (file && file != 'empty') {
|
||||
if (file.indexOf('soondesign_file:') === 0 && typeof sessionStorage !== 'undefined') {
|
||||
try {
|
||||
var metaRaw = sessionStorage.getItem('soondesign_open_meta');
|
||||
if (metaRaw) {
|
||||
window._soonFileMeta = JSON.parse(metaRaw);
|
||||
sessionStorage.removeItem('soondesign_open_meta');
|
||||
}
|
||||
} catch (e) {}
|
||||
if (typeof window.soonConsumeOpenMeta === 'function') {
|
||||
window.soonConsumeOpenMeta(file);
|
||||
}
|
||||
var p = window.openFile(file);
|
||||
if (p && p.then) p.then(function() {}, function() {});
|
||||
window.openFile(file);
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -2048,7 +2087,7 @@ if (typeof ipcRenderer !== 'undefined' && ipcRenderer) {
|
||||
}
|
||||
function onCloseConfirm() {
|
||||
layer.confirm(
|
||||
'<font style="color:black">' + language_str('whetherSave') + '</font>',
|
||||
language_str('whetherSave'),
|
||||
{
|
||||
btn: [language_str('save'), language_str('noSave'), language_str('cancel')],
|
||||
btn3: function (index, layero) { }
|
||||
|
||||
@@ -333,6 +333,7 @@ window.display_func = function display_func(img1, img2, img3) {
|
||||
|
||||
layer.open({
|
||||
type: 1,
|
||||
skin: 'soon-layer',
|
||||
area: [w, h],
|
||||
title: language_str('display'), //不显示标题栏"预览"
|
||||
shadeClose: true, //点击遮罩关闭
|
||||
@@ -1087,6 +1088,9 @@ function cloudSaveSuccessLabel(res, fallback) {
|
||||
|
||||
function onCloudWriteDone(res, fallback, callback) {
|
||||
applyCloudWriteResult(res, fallback);
|
||||
if (res && res.fileKey && typeof window.soonClearPendingImport === 'function') {
|
||||
window.soonClearPendingImport();
|
||||
}
|
||||
layer.msg(language_str('saveSucc') + cloudSaveSuccessLabel(res, fallback));
|
||||
if (callback && typeof callback === 'function') callback();
|
||||
}
|
||||
@@ -1095,6 +1099,15 @@ function guardWebSave() {
|
||||
return typeof window.soonGuardCloudSave === 'function' ? window.soonGuardCloudSave() : true;
|
||||
}
|
||||
|
||||
function reportSaveError(err) {
|
||||
if (err && err.status) return;
|
||||
if (typeof window.soonShowApiError === 'function') {
|
||||
window.soonShowApiError({ status: err && err.status, message: (err && err.message) || '保存失败' });
|
||||
} else if (typeof window.soonToast === 'function') {
|
||||
window.soonToast('保存失败', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function saveAs(op1, callback) {
|
||||
// 临时移除辅助线,保存后再恢复
|
||||
let guideLines1 = []
|
||||
@@ -1143,7 +1156,7 @@ function saveAs(op1, callback) {
|
||||
if (window.platformBridge && window.platformBridge.writeFile) {
|
||||
window.platformBridge.writeFile(fp, content).then(function (res) {
|
||||
onCloudWriteDone(res, fp, callback);
|
||||
}).catch(function () {});
|
||||
}).catch(reportSaveError);
|
||||
return;
|
||||
}
|
||||
var fs = typeof require !== 'undefined' ? require('fs') : null;
|
||||
@@ -1155,7 +1168,7 @@ function saveAs(op1, callback) {
|
||||
if (callback && typeof callback === 'function') callback();
|
||||
}
|
||||
})
|
||||
.catch(function(err) {});
|
||||
.catch(reportSaveError);
|
||||
}
|
||||
|
||||
function save(op1, callback) {
|
||||
@@ -1198,7 +1211,7 @@ function save(op1, callback) {
|
||||
if (window.platformBridge && window.platformBridge.writeFile) {
|
||||
window.platformBridge.writeFile(openAs.name, content).then(function (res) {
|
||||
onCloudWriteDone(res, openAs.name, callback);
|
||||
}).catch(function () {});
|
||||
}).catch(reportSaveError);
|
||||
return;
|
||||
}
|
||||
var fs = typeof require !== 'undefined' ? require('fs') : null;
|
||||
@@ -1222,7 +1235,7 @@ function save(op1, callback) {
|
||||
if (window.platformBridge && window.platformBridge.writeFile) {
|
||||
window.platformBridge.writeFile(fp, content).then(function (res) {
|
||||
onCloudWriteDone(res, fp, callback);
|
||||
}).catch(function () {});
|
||||
}).catch(reportSaveError);
|
||||
return;
|
||||
}
|
||||
var fs = typeof require !== 'undefined' ? require('fs') : null;
|
||||
@@ -1234,7 +1247,7 @@ function save(op1, callback) {
|
||||
if (callback && typeof callback === 'function') callback();
|
||||
}
|
||||
})
|
||||
.catch(function(err) {});
|
||||
.catch(reportSaveError);
|
||||
}
|
||||
|
||||
window.saveHistory = function saveHistory() {
|
||||
@@ -1287,7 +1300,10 @@ window.openFile = function open(file, jAlready) {
|
||||
if (!file) return;
|
||||
if (typeof window.soonBindFileMeta === 'function') window.soonBindFileMeta(file);
|
||||
function doOpenWithJson(j) {
|
||||
if (!j || !background_image) return;
|
||||
if (!j || !background_image) {
|
||||
if (typeof window.soonReportOpenLoadError === 'function') window.soonReportOpenLoadError('模板/文件加载失败');
|
||||
return;
|
||||
}
|
||||
openAs.name = file;
|
||||
var left = background_image.left
|
||||
let top = background_image.top
|
||||
@@ -1438,6 +1454,7 @@ window.openFile = function open(file, jAlready) {
|
||||
recordJson = recordJson2
|
||||
step = step2
|
||||
}
|
||||
if (typeof window.soonTryConsumeCloudImport === 'function') window.soonTryConsumeCloudImport(file);
|
||||
}
|
||||
|
||||
canvas1.loadFromJSON(j.f, function () {
|
||||
@@ -1482,7 +1499,11 @@ window.openFile = function open(file, jAlready) {
|
||||
if (j) {
|
||||
doOpenWithJson(j);
|
||||
openAs.name = file;
|
||||
} else if (typeof window.soonReportOpenLoadError === 'function') {
|
||||
window.soonReportOpenLoadError('模板/文件加载失败');
|
||||
}
|
||||
}).catch(function () {
|
||||
if (typeof window.soonReportOpenLoadError === 'function') window.soonReportOpenLoadError('模板/文件加载失败');
|
||||
});
|
||||
}
|
||||
try {
|
||||
|
||||
@@ -1299,7 +1299,7 @@ $('#open').click(function () {
|
||||
return;
|
||||
}
|
||||
layer.confirm(
|
||||
'<font style="color:black">' + language_str('whetherSave') + '</font>',
|
||||
language_str('whetherSave'),
|
||||
function (index) {
|
||||
if (typeof window.output === 'function') {
|
||||
window.output(OpenDialog);
|
||||
@@ -1326,18 +1326,14 @@ $('#open').click(function () {
|
||||
if (typeof window.openFile === 'function') window.openFile(fileKey, j);
|
||||
}
|
||||
function importAndOpen(j, fileName) {
|
||||
var tok = typeof window.soonGetAccessToken === 'function'
|
||||
? window.soonGetAccessToken()
|
||||
: (localStorage.getItem('soon_access') || '');
|
||||
if (!tok && typeof window.soonPutSoonSession === 'function') {
|
||||
if (typeof window.soonPutSoonSession === 'function') {
|
||||
var localKey = window.soonPutSoonSession(j, fileName || 'design.soon');
|
||||
if (localKey) openWithKey(localKey, j);
|
||||
return;
|
||||
if (!localKey) return;
|
||||
if (typeof window.soonScheduleCloudImport === 'function') {
|
||||
window.soonScheduleCloudImport(localKey, fileName || 'design.soon');
|
||||
}
|
||||
openWithKey(localKey, j);
|
||||
}
|
||||
if (!window.platformBridge || !window.platformBridge.importSoonFile) return;
|
||||
window.platformBridge.importSoonFile(fileName || 'design.soon', j).then(function(res) {
|
||||
if (res && res.fileKey) openWithKey(res.fileKey, j);
|
||||
}).catch(function () {});
|
||||
}
|
||||
if (file && file.text) {
|
||||
file.text().then(function(t) {
|
||||
@@ -1364,7 +1360,7 @@ $('#new').click(function () {
|
||||
return;
|
||||
}
|
||||
layer.confirm(
|
||||
'<font style="color:black">' + language_str('whetherSave') + '</font>',
|
||||
language_str('whetherSave'),
|
||||
function (index) {
|
||||
if (typeof window.output === 'function') {
|
||||
window.output(goFirstPage);
|
||||
|
||||
+26
-49
@@ -512,41 +512,26 @@ layui.use(['layer', 'form', 'jquery'], function () {
|
||||
function resolveTemplateItem(m, base) {
|
||||
var type = Number(m.type) || 1;
|
||||
var id = m.id;
|
||||
var v = m.updated_at ? ('?v=' + encodeURIComponent(m.updated_at)) : '';
|
||||
return {
|
||||
id: id,
|
||||
name: m.name || m.title || '模板',
|
||||
type: type,
|
||||
thumbSrc: id ? (base + '/templates/' + id + '/thumb') : templateFallbackThumb(type),
|
||||
file_url: id ? (base + '/templates/' + id + '/file') : (m.file_url || '')
|
||||
thumbSrc: id ? (base + '/templates/' + id + '/thumb' + v) : templateFallbackThumb(type)
|
||||
};
|
||||
}
|
||||
|
||||
function openTemplateItem(m) {
|
||||
var type = Number(m.type) || 1;
|
||||
function openWithJson(j) {
|
||||
if (!j) {
|
||||
openDesign(type, '');
|
||||
return;
|
||||
}
|
||||
var key = 'soondesign_session:tpl-' + Date.now();
|
||||
try {
|
||||
sessionStorage.setItem(key, JSON.stringify(j));
|
||||
openDesign(type, key);
|
||||
} catch (e) {
|
||||
openDesign(type, '');
|
||||
}
|
||||
}
|
||||
var fileUrl = m.file_url || '';
|
||||
if (!fileUrl) {
|
||||
openDesign(type, '');
|
||||
var id = m && m.id;
|
||||
if (!id) {
|
||||
if (typeof window.soonToast === 'function') window.soonToast('模板不可用', 'error');
|
||||
return;
|
||||
}
|
||||
fetch(fileUrl).then(function (r) {
|
||||
if (!r.ok) throw new Error('fetch');
|
||||
return r.json();
|
||||
}).then(openWithJson).catch(function () {
|
||||
openDesign(type, '');
|
||||
});
|
||||
var templateKey = typeof window.soonMakeTemplateKey === 'function'
|
||||
? window.soonMakeTemplateKey(id)
|
||||
: ('soondesign_template:' + id);
|
||||
openDesign(type, templateKey);
|
||||
}
|
||||
|
||||
function templateCardHtml(m, itemIndex) {
|
||||
@@ -614,7 +599,9 @@ layui.use(['layer', 'form', 'jquery'], function () {
|
||||
renderTemplatePager();
|
||||
}
|
||||
|
||||
async function loadTemplates() {
|
||||
var _templatesLoadedAt = 0;
|
||||
|
||||
async function loadTemplates(force) {
|
||||
|
||||
var grid = document.getElementById('templatesGrid');
|
||||
|
||||
@@ -622,6 +609,10 @@ layui.use(['layer', 'form', 'jquery'], function () {
|
||||
|
||||
if (!grid) return;
|
||||
|
||||
if (!force && _templatesLoadedAt && (Date.now() - _templatesLoadedAt) < 60000) {
|
||||
return;
|
||||
}
|
||||
|
||||
var base = (window.SOON_DEPLOY_CONFIG && window.SOON_DEPLOY_CONFIG.api_v1_base) || '';
|
||||
|
||||
if (!base) {
|
||||
@@ -655,6 +646,7 @@ layui.use(['layer', 'form', 'jquery'], function () {
|
||||
templateListState.total = templateListState.items.length;
|
||||
templateListState.page = 1;
|
||||
renderTemplatesPage();
|
||||
_templatesLoadedAt = Date.now();
|
||||
|
||||
} catch (e) {
|
||||
|
||||
@@ -670,6 +662,14 @@ layui.use(['layer', 'form', 'jquery'], function () {
|
||||
|
||||
loadTemplates();
|
||||
|
||||
window.soonReloadTemplates = function () { loadTemplates(true); };
|
||||
document.addEventListener('visibilitychange', function () {
|
||||
if (document.visibilityState === 'visible') loadTemplates(false);
|
||||
});
|
||||
window.addEventListener('pageshow', function (e) {
|
||||
if (e.persisted) loadTemplates(false);
|
||||
});
|
||||
|
||||
var templatePrevNav = document.getElementById('templatePrev');
|
||||
var templateNextNav = document.getElementById('templateNext');
|
||||
if (templatePrevNav) {
|
||||
@@ -879,30 +879,7 @@ layui.use(['layer', 'form', 'jquery'], function () {
|
||||
|
||||
}
|
||||
|
||||
var tok = typeof window.soonGetAccessToken === 'function'
|
||||
? window.soonGetAccessToken()
|
||||
: (localStorage.getItem('soon_access') || '');
|
||||
|
||||
if (!tok && typeof window.soonOpenSoonJsonLocally === 'function') {
|
||||
window.soonOpenSoonJsonLocally(soonData, fileName);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!window.platformBridge || !window.platformBridge.importSoonFile) return;
|
||||
|
||||
try {
|
||||
|
||||
var res = await window.platformBridge.importSoonFile(fileName, soonData);
|
||||
|
||||
if (!res || !res.fileKey) return;
|
||||
|
||||
var type = soonData.soonType ? soonData.soonType : (soonData.backBlackPic ? 2 : 1);
|
||||
|
||||
openDesign(type, res.fileKey);
|
||||
|
||||
loadHistory();
|
||||
|
||||
} catch (err) { /* errors shown by bridge */ }
|
||||
window.soonOpenSoonJsonLocally(soonData, fileName);
|
||||
|
||||
}).catch(function (err) { console.log(err); });
|
||||
|
||||
|
||||
@@ -244,12 +244,46 @@
|
||||
if (key && key.indexOf('soondesign_file:') === 0 && typeof fetch !== 'undefined') {
|
||||
var fileMatch = key.match(/^soondesign_file:(\d+)/);
|
||||
if (fileMatch && getAccessToken()) {
|
||||
return authedFetch('files/' + fileMatch[1] + '/download', { headers: { Accept: 'application/json' } })
|
||||
return authedFetch('files/' + fileMatch[1], { headers: { Accept: 'application/json' } })
|
||||
.then(function (response) {
|
||||
if (!response.ok) return null;
|
||||
return response.text().then(function (text) {
|
||||
try { return JSON.parse(text); } catch (e) { return null; }
|
||||
});
|
||||
return response.json();
|
||||
})
|
||||
.then(function (payload) {
|
||||
if (!payload || !payload.ok || !payload.data) return null;
|
||||
var raw = payload.data.json;
|
||||
if (typeof raw === 'string') {
|
||||
try { return JSON.parse(raw); } catch (e) { return null; }
|
||||
}
|
||||
return raw && typeof raw === 'object' ? raw : null;
|
||||
})
|
||||
.catch(function () { return null; });
|
||||
}
|
||||
}
|
||||
if (key && key.indexOf('soondesign_template:') === 0 && typeof fetch !== 'undefined') {
|
||||
var tplMatch = key.match(/^soondesign_template:(\d+)/);
|
||||
if (tplMatch) {
|
||||
var apiBase = (window.SOON_DEPLOY_CONFIG && window.SOON_DEPLOY_CONFIG.api_v1_base) || '';
|
||||
if (!apiBase) return Promise.resolve(null);
|
||||
return fetch(apiBase + '/templates/' + tplMatch[1], { headers: { Accept: 'application/json' } })
|
||||
.then(function (response) {
|
||||
if (!response.ok) return null;
|
||||
return response.json();
|
||||
})
|
||||
.then(function (payload) {
|
||||
if (!payload || !payload.ok || !payload.data) return null;
|
||||
if (payload.data.id) {
|
||||
window._soonTemplateMeta = {
|
||||
id: payload.data.id,
|
||||
name: payload.data.name || '',
|
||||
type: payload.data.type
|
||||
};
|
||||
}
|
||||
var raw = payload.data.json;
|
||||
if (typeof raw === 'string') {
|
||||
try { return JSON.parse(raw); } catch (e) { return null; }
|
||||
}
|
||||
return raw && typeof raw === 'object' ? raw : null;
|
||||
})
|
||||
.catch(function () { return null; });
|
||||
}
|
||||
@@ -294,18 +328,51 @@
|
||||
showSaveDialog: function (options) {
|
||||
var raw = (options && options.defaultPath) ? options.defaultPath : '';
|
||||
var defaultName = normalizeSoonName(raw || 'design.soon');
|
||||
if (typeof window.soonDisplayFileName === 'function' && raw.indexOf('soondesign_file:') === 0) {
|
||||
if (typeof window.soonDisplayFileName === 'function' &&
|
||||
(raw.indexOf('soondesign_file:') === 0 || raw.indexOf('soondesign_template:') === 0)) {
|
||||
defaultName = window.soonDisplayFileName(raw);
|
||||
}
|
||||
var title = (options && options.title) ? options.title : '保存到云端';
|
||||
return new Promise(function (resolve) {
|
||||
var name = typeof prompt === 'function' ? prompt('保存为文件名(如 xxx.soon)', defaultName) : defaultName;
|
||||
if (name === null) {
|
||||
resolve({ canceled: true });
|
||||
if (typeof layer === 'undefined' || !layer.open) {
|
||||
var fpFallback = defaultName;
|
||||
resolve({ canceled: false, filePath: fpFallback, useCloud: true });
|
||||
return;
|
||||
}
|
||||
var fp = (name && String(name).trim()) ? String(name).trim() : defaultName;
|
||||
fp = normalizeSoonName(fp);
|
||||
resolve({ canceled: false, filePath: fp, useCloud: true });
|
||||
var esc = function (s) {
|
||||
return String(s || '').replace(/&/g, '&').replace(/</g, '<').replace(/"/g, '"');
|
||||
};
|
||||
var html = '<div class="soon-dialog-body" style="padding:16px 20px;">' +
|
||||
'<label style="display:block;margin-bottom:8px;color:#b8c0c8;font-size:13px;">文件名</label>' +
|
||||
'<input type="text" id="soonSaveDialogInput" class="layui-input" value="' + esc(defaultName) + '" ' +
|
||||
'style="background:#2b3036;border-color:rgba(255,255,255,0.08);color:#eef1f4;">' +
|
||||
'</div>';
|
||||
layer.open({
|
||||
type: 1,
|
||||
skin: 'soon-layer',
|
||||
title: title,
|
||||
area: ['360px', 'auto'],
|
||||
shadeClose: false,
|
||||
content: html,
|
||||
btn: ['确定', '取消'],
|
||||
btnAlign: 'r',
|
||||
yes: function (index) {
|
||||
var input = document.getElementById('soonSaveDialogInput');
|
||||
var name = input && input.value ? String(input.value).trim() : defaultName;
|
||||
if (!name) name = defaultName;
|
||||
name = normalizeSoonName(name);
|
||||
layer.close(index);
|
||||
resolve({ canceled: false, filePath: name, useCloud: true });
|
||||
},
|
||||
btn2: function (index) {
|
||||
layer.close(index);
|
||||
resolve({ canceled: true });
|
||||
},
|
||||
cancel: function (index) {
|
||||
layer.close(index);
|
||||
resolve({ canceled: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
writeFile: function (pathOrHandle, content) {
|
||||
@@ -383,16 +450,11 @@
|
||||
getSystemFonts: getSystemFonts,
|
||||
openDesignPage: function (file, type) {
|
||||
var t = type || 1;
|
||||
if (file && typeof sessionStorage !== 'undefined') {
|
||||
try {
|
||||
sessionStorage.setItem('soondesign_open_file', file);
|
||||
sessionStorage.setItem('soondesign_open_type', String(t));
|
||||
if (file.indexOf('soondesign_file:') === 0 && window._soonFileMeta) {
|
||||
sessionStorage.setItem('soondesign_open_meta', JSON.stringify(window._soonFileMeta));
|
||||
}
|
||||
} catch (e) {}
|
||||
var key = file || '';
|
||||
if (typeof window.soonSyncOpenNavigation === 'function') {
|
||||
window.soonSyncOpenNavigation(key, t);
|
||||
}
|
||||
var fileParam = file ? encodeURIComponent(file) : '';
|
||||
var fileParam = key ? encodeURIComponent(key) : '';
|
||||
navigateToPage('design' + t + '.web.html?file=' + fileParam + '&type=' + t);
|
||||
},
|
||||
openFirstPage: function () {
|
||||
@@ -453,6 +515,7 @@
|
||||
showOpenDialog: bridge.showOpenDialog,
|
||||
showSaveDialog: bridge.showSaveDialog
|
||||
};
|
||||
window.soonShowSaveDialog = bridge.showSaveDialog;
|
||||
window.path = pathStub;
|
||||
window.fs = null;
|
||||
window.remote = null;
|
||||
|
||||
@@ -938,6 +938,148 @@ body.soon-admin-page .soon-admin-date-wrap input[type="date"].soon-input::-webki
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.soon-admin-form--tpl { padding: var(--soon-space-md) var(--soon-space-lg); }
|
||||
.soon-admin-tpl-modal {
|
||||
padding: var(--soon-space-lg);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.soon-admin-tpl-card {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
gap: var(--soon-space-md);
|
||||
width: 100%;
|
||||
margin: 0 0 var(--soon-space-md);
|
||||
padding: var(--soon-space-md);
|
||||
text-align: left;
|
||||
background: var(--soon-bg-elevated);
|
||||
border: 1px solid var(--soon-border);
|
||||
border-radius: var(--soon-radius-md);
|
||||
cursor: default;
|
||||
color: inherit;
|
||||
transition: border-color 0.15s ease, box-shadow 0.15s ease;
|
||||
}
|
||||
.soon-admin-tpl-card.is-ready {
|
||||
cursor: zoom-in;
|
||||
}
|
||||
.soon-admin-tpl-card.is-ready:hover {
|
||||
border-color: rgba(0, 150, 136, 0.45);
|
||||
box-shadow: 0 0 0 1px rgba(0, 150, 136, 0.12);
|
||||
}
|
||||
.soon-admin-tpl-card__media {
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
width: 96px;
|
||||
height: 120px;
|
||||
border-radius: var(--soon-radius-sm);
|
||||
background: var(--soon-bg-base);
|
||||
border: 1px solid var(--soon-border);
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.soon-admin-tpl-card__media img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
.soon-admin-tpl-card__placeholder {
|
||||
padding: 0 8px;
|
||||
font-size: 11px;
|
||||
line-height: 1.4;
|
||||
text-align: center;
|
||||
color: var(--soon-text-muted);
|
||||
}
|
||||
.soon-admin-tpl-card__hint {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
padding: 4px 6px;
|
||||
font-size: 10px;
|
||||
text-align: center;
|
||||
color: #fff;
|
||||
background: linear-gradient(transparent, rgba(0, 0, 0, 0.72));
|
||||
}
|
||||
.soon-admin-tpl-card__info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.soon-admin-tpl-card__name {
|
||||
font-size: var(--soon-font-md);
|
||||
font-weight: 600;
|
||||
color: var(--soon-text-strong);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.soon-admin-tpl-card__type {
|
||||
display: inline-flex;
|
||||
align-self: flex-start;
|
||||
padding: 2px 10px;
|
||||
font-size: var(--soon-font-xs);
|
||||
color: var(--soon-text-muted);
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
border: 1px solid var(--soon-border);
|
||||
border-radius: 999px;
|
||||
}
|
||||
.soon-admin-tpl-picker {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--soon-space-sm);
|
||||
margin-bottom: var(--soon-space-md);
|
||||
cursor: pointer;
|
||||
}
|
||||
.soon-admin-tpl-picker__input {
|
||||
position: absolute;
|
||||
width: 0;
|
||||
height: 0;
|
||||
opacity: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
.soon-admin-tpl-picker__name {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
font-size: var(--soon-font-sm);
|
||||
color: var(--soon-text-muted);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.soon-admin-tpl-options {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: var(--soon-space-md);
|
||||
margin-bottom: var(--soon-space-md);
|
||||
}
|
||||
.soon-admin-tpl-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin: 0;
|
||||
font-size: var(--soon-font-sm);
|
||||
color: var(--soon-text-muted);
|
||||
}
|
||||
.soon-admin-tpl-option .soon-input {
|
||||
width: 88px;
|
||||
}
|
||||
.soon-admin-tpl-option--check {
|
||||
margin-left: auto;
|
||||
color: var(--soon-text);
|
||||
}
|
||||
.soon-admin-tpl-modal__actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding-top: var(--soon-space-md);
|
||||
border-top: 1px solid var(--soon-border);
|
||||
}
|
||||
|
||||
.soon-admin-detail-empty {
|
||||
margin: 0;
|
||||
font-size: var(--soon-font-sm);
|
||||
@@ -988,3 +1130,16 @@ body.soon-admin-page .layui-layer-soon-layer .layui-layer-ico {
|
||||
filter: brightness(0) invert(1);
|
||||
opacity: 0.92;
|
||||
}
|
||||
|
||||
.layui-layer.soon-layer.soon-layer--tpl,
|
||||
.layui-layer-soon-layer.soon-layer--tpl {
|
||||
border-radius: var(--soon-radius-md) !important;
|
||||
}
|
||||
|
||||
.layui-layer.soon-layer.soon-layer--tpl .layui-layer-content,
|
||||
.layui-layer-soon-layer.soon-layer--tpl .layui-layer-content {
|
||||
overflow: hidden !important;
|
||||
max-height: none !important;
|
||||
height: auto !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
@@ -150,7 +150,7 @@
|
||||
opts = opts || {};
|
||||
layer.open({
|
||||
type: 1,
|
||||
skin: 'soon-layer',
|
||||
skin: opts.skin || 'soon-layer',
|
||||
title: title,
|
||||
area: opts.area || ['420px', 'auto'],
|
||||
maxWidth: opts.maxWidth || '96vw',
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
var I = function () { return window.AdminI18n; };
|
||||
var Form = function () { return window.AdminForm; };
|
||||
|
||||
var MODAL_OPTS = { area: ['480px', 'auto'], skin: 'soon-layer soon-layer--tpl', maxWidth: '94vw' };
|
||||
|
||||
function activeBadge(v) {
|
||||
var on = v === true || parseInt(v, 10) === 1;
|
||||
return I().badgeStatus(on ? 'active' : 'disabled');
|
||||
@@ -23,6 +25,99 @@
|
||||
return (n / 1048576).toFixed(1) + ' MB';
|
||||
}
|
||||
|
||||
function getAdminBase() {
|
||||
return ((window.SOON_DEPLOY_CONFIG && window.SOON_DEPLOY_CONFIG.api_admin_base) || '').replace(/\/+$/, '');
|
||||
}
|
||||
|
||||
function revokeBlob(state) {
|
||||
if (state && state._blobUrl) {
|
||||
try { URL.revokeObjectURL(state._blobUrl); } catch (e) { /* ignore */ }
|
||||
state._blobUrl = '';
|
||||
}
|
||||
}
|
||||
|
||||
function nameFromFileName(fileName) {
|
||||
return String(fileName || '').replace(/\.soon$/i, '').replace(/[_-]+/g, ' ').trim();
|
||||
}
|
||||
|
||||
function parseSoonMeta(text, fileName) {
|
||||
var data;
|
||||
try {
|
||||
data = JSON.parse(text);
|
||||
} catch (e) {
|
||||
throw new Error('invalid_json');
|
||||
}
|
||||
if (!data || typeof data !== 'object') throw new Error('invalid_json');
|
||||
|
||||
var type = 1;
|
||||
if (Number(data.soonType) === 2 || data.backBlackPic) type = 2;
|
||||
|
||||
var thumbRaw = '';
|
||||
if (data.frontDisplayPic && typeof data.frontDisplayPic === 'string') {
|
||||
var candidate = data.frontDisplayPic.trim();
|
||||
if (candidate.indexOf('data:image/') === 0) thumbRaw = candidate;
|
||||
}
|
||||
|
||||
var name = '';
|
||||
if (typeof data.name === 'string' && data.name.trim()) name = data.name.trim();
|
||||
else if (typeof data.title === 'string' && data.title.trim()) name = data.title.trim();
|
||||
else name = nameFromFileName(fileName);
|
||||
if (!name) name = '未命名模板';
|
||||
|
||||
return { name: name, type: type, thumbRaw: thumbRaw, content: text };
|
||||
}
|
||||
|
||||
function shrinkDataUrl(dataUrl, maxW, quality, done) {
|
||||
if (!dataUrl) {
|
||||
done('');
|
||||
return;
|
||||
}
|
||||
var img = new Image();
|
||||
img.onload = function () {
|
||||
var w = img.naturalWidth || img.width;
|
||||
var h = img.naturalHeight || img.height;
|
||||
if (!w || !h) {
|
||||
done(dataUrl);
|
||||
return;
|
||||
}
|
||||
if (w <= maxW) {
|
||||
done(dataUrl);
|
||||
return;
|
||||
}
|
||||
var nh = Math.max(1, Math.round(h * (maxW / w)));
|
||||
var canvas = document.createElement('canvas');
|
||||
canvas.width = maxW;
|
||||
canvas.height = nh;
|
||||
canvas.getContext('2d').drawImage(img, 0, 0, maxW, nh);
|
||||
try {
|
||||
done(canvas.toDataURL('image/jpeg', quality));
|
||||
} catch (e) {
|
||||
done(dataUrl);
|
||||
}
|
||||
};
|
||||
img.onerror = function () { done(''); };
|
||||
img.src = dataUrl;
|
||||
}
|
||||
|
||||
function setPreviewImage(modalState, src, renderPreview) {
|
||||
modalState.thumbView = src || '';
|
||||
renderPreview();
|
||||
}
|
||||
|
||||
function applyThumbRaw(modalState, thumbRaw, renderPreview) {
|
||||
if (!thumbRaw) {
|
||||
setPreviewImage(modalState, '', renderPreview);
|
||||
return;
|
||||
}
|
||||
if (thumbRaw.length < 700000) {
|
||||
setPreviewImage(modalState, thumbRaw, renderPreview);
|
||||
return;
|
||||
}
|
||||
shrinkDataUrl(thumbRaw, 360, 0.82, function (small) {
|
||||
setPreviewImage(modalState, small || thumbRaw, renderPreview);
|
||||
});
|
||||
}
|
||||
|
||||
function readFileText(input) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
var file = input && input.files && input.files[0];
|
||||
@@ -31,106 +126,195 @@
|
||||
return;
|
||||
}
|
||||
var reader = new FileReader();
|
||||
reader.onload = function () { resolve(String(reader.result || '')); };
|
||||
reader.onload = function () { resolve({ text: String(reader.result || ''), fileName: file.name }); };
|
||||
reader.onerror = function () { reject(new Error('read_failed')); };
|
||||
reader.readAsText(file, 'UTF-8');
|
||||
});
|
||||
}
|
||||
|
||||
function defaultNameFromFile(input) {
|
||||
var file = input && input.files && input.files[0];
|
||||
if (!file) return '';
|
||||
return file.name.replace(/\.soon$/i, '').replace(/[_-]+/g, ' ').trim();
|
||||
function fetchAdminThumb(plan, modalState, renderPreview) {
|
||||
if (!plan || !plan.id || !Api().getToken()) {
|
||||
renderPreview();
|
||||
return;
|
||||
}
|
||||
revokeBlob(modalState);
|
||||
window.fetch(getAdminBase() + '/templates/' + plan.id + '/thumb?_=' + Date.now(), {
|
||||
headers: { Authorization: 'Bearer ' + Api().getToken() },
|
||||
}).then(function (r) {
|
||||
if (!r.ok) throw new Error('thumb');
|
||||
return r.blob();
|
||||
}).then(function (blob) {
|
||||
modalState._blobUrl = URL.createObjectURL(blob);
|
||||
setPreviewImage(modalState, modalState._blobUrl, renderPreview);
|
||||
}).catch(function () {
|
||||
renderPreview();
|
||||
});
|
||||
}
|
||||
|
||||
function uploadFields(required) {
|
||||
return Form().panel('模板文件',
|
||||
Form().field('选择 .soon 文件',
|
||||
'<input type="file" id="tplFile" class="soon-input soon-input--sm" accept=".soon,application/json">' +
|
||||
(required ? '' : '<p class="soon-admin-form-hint soon-admin-form-hint--tight">留空则不替换已有文件</p>'),
|
||||
required ? '须为有效的 .soon JSON 文件' : '')
|
||||
);
|
||||
function openThumbPreview(src, title) {
|
||||
if (!src || typeof layer === 'undefined') return;
|
||||
layer.photos({
|
||||
photos: {
|
||||
title: title || '模板预览',
|
||||
start: 0,
|
||||
data: [{ alt: title || '模板预览', src: src }],
|
||||
},
|
||||
anim: 5,
|
||||
shade: 0.88,
|
||||
});
|
||||
}
|
||||
|
||||
function metaFields(plan) {
|
||||
plan = plan || {};
|
||||
return Form().panel('展示设置',
|
||||
Form().field('名称', Form().input('tplName', { value: plan.name || '' })) +
|
||||
'<div class="soon-admin-form-grid">' +
|
||||
Form().field('排序', Form().input('tplSort', { type: 'number', value: plan.sort_order != null ? plan.sort_order : 0 })) +
|
||||
Form().field('类型', '<input class="soon-input soon-input--sm" value="' + I().esc(typeLabel(plan.type)) + '" disabled>') +
|
||||
'</div>' +
|
||||
'<label class="soon-admin-check"><input type="checkbox" id="tplActive"' +
|
||||
(plan.is_active !== false ? ' checked' : '') + '> 上架展示</label>'
|
||||
);
|
||||
function modalHtml(opts) {
|
||||
opts = opts || {};
|
||||
var isEdit = opts.mode === 'edit';
|
||||
var plan = opts.plan || {};
|
||||
var pickLabel = isEdit ? '替换文件' : '选择文件';
|
||||
|
||||
return '<div class="soon-admin-tpl-modal">' +
|
||||
'<button type="button" id="tplThumbBtn" class="soon-admin-tpl-card" disabled aria-label="预览缩略图">' +
|
||||
'<span class="soon-admin-tpl-card__media">' +
|
||||
'<img id="tplThumb" alt="" hidden>' +
|
||||
'<span id="tplThumbEmpty" class="soon-admin-tpl-card__placeholder">选择 .soon 后显示预览</span>' +
|
||||
'<span id="tplThumbHint" class="soon-admin-tpl-card__hint" hidden>点击放大</span></span>' +
|
||||
'<span class="soon-admin-tpl-card__info">' +
|
||||
'<span id="tplNameDisp" class="soon-admin-tpl-card__name">' + I().esc(isEdit ? (plan.name || '—') : '—') + '</span>' +
|
||||
'<span id="tplTypeDisp" class="soon-admin-tpl-card__type">' +
|
||||
(isEdit ? I().esc(typeLabel(plan.type)) : '—') + '</span></span></button>' +
|
||||
'<label class="soon-admin-tpl-picker">' +
|
||||
'<input type="file" id="tplFile" class="soon-admin-tpl-picker__input" accept=".soon,application/json">' +
|
||||
'<span class="soon-btn soon-btn--sm soon-btn--ghost">' + I().esc(pickLabel) + '</span>' +
|
||||
'<span id="tplFileLabel" class="soon-admin-tpl-picker__name">' +
|
||||
(isEdit ? I().esc(plan.name + '.soon') : '未选择') + '</span></label>' +
|
||||
'<div class="soon-admin-tpl-options">' +
|
||||
'<label class="soon-admin-tpl-option"><span>排序</span>' +
|
||||
Form().input('tplSort', { type: 'number', value: plan.sort_order != null ? plan.sort_order : 0 }) + '</label>' +
|
||||
'<label class="soon-admin-tpl-option soon-admin-tpl-option--check">' +
|
||||
'<input type="checkbox" id="tplActive"' + (plan.is_active !== false ? ' checked' : '') + '>' +
|
||||
'<span>首页上架</span></label></div>' +
|
||||
'<div class="soon-admin-tpl-modal__actions">' +
|
||||
'<button type="button" class="soon-btn soon-btn--sm" id="adminModalSave">' +
|
||||
(isEdit ? '保存' : '上传') + '</button></div></div>';
|
||||
}
|
||||
|
||||
function bindTplModal(opts, modalState) {
|
||||
var fileEl = Ui().el('tplFile');
|
||||
var thumbBtn = Ui().el('tplThumbBtn');
|
||||
var thumbEl = Ui().el('tplThumb');
|
||||
var thumbEmptyEl = Ui().el('tplThumbEmpty');
|
||||
var thumbHintEl = Ui().el('tplThumbHint');
|
||||
var nameEl = Ui().el('tplNameDisp');
|
||||
var typeEl = Ui().el('tplTypeDisp');
|
||||
var labelEl = Ui().el('tplFileLabel');
|
||||
var plan = opts.plan;
|
||||
|
||||
function renderPreview() {
|
||||
var hasMeta = !!(modalState.name || modalState.type);
|
||||
var src = modalState.thumbView || '';
|
||||
nameEl.textContent = modalState.name || '—';
|
||||
typeEl.textContent = hasMeta ? typeLabel(modalState.type) : '—';
|
||||
|
||||
if (src) {
|
||||
thumbEl.src = src;
|
||||
thumbEl.hidden = false;
|
||||
thumbEmptyEl.hidden = true;
|
||||
thumbHintEl.hidden = false;
|
||||
thumbBtn.disabled = false;
|
||||
thumbBtn.classList.add('is-ready');
|
||||
} else {
|
||||
thumbEl.hidden = true;
|
||||
thumbEl.removeAttribute('src');
|
||||
thumbEmptyEl.hidden = false;
|
||||
thumbEmptyEl.textContent = hasMeta ? '无缩略图' : '选择 .soon 后显示预览';
|
||||
thumbHintEl.hidden = true;
|
||||
thumbBtn.disabled = true;
|
||||
thumbBtn.classList.remove('is-ready');
|
||||
}
|
||||
}
|
||||
|
||||
if (thumbBtn) {
|
||||
thumbBtn.onclick = function () {
|
||||
if (!modalState.thumbView) return;
|
||||
openThumbPreview(modalState.thumbView, modalState.name);
|
||||
};
|
||||
}
|
||||
|
||||
if (opts.mode === 'edit' && plan) {
|
||||
modalState.name = plan.name;
|
||||
modalState.type = plan.type;
|
||||
modalState.thumbView = '';
|
||||
modalState.content = null;
|
||||
renderPreview();
|
||||
fetchAdminThumb(plan, modalState, renderPreview);
|
||||
}
|
||||
|
||||
if (!fileEl) return;
|
||||
|
||||
fileEl.onchange = function () {
|
||||
var file = fileEl.files && fileEl.files[0];
|
||||
if (!file) return;
|
||||
labelEl.textContent = file.name;
|
||||
readFileText(fileEl).then(function (payload) {
|
||||
var meta = parseSoonMeta(payload.text, payload.fileName);
|
||||
modalState.name = meta.name;
|
||||
modalState.type = meta.type;
|
||||
modalState.content = meta.content;
|
||||
applyThumbRaw(modalState, meta.thumbRaw, renderPreview);
|
||||
}).catch(function (e) {
|
||||
fileEl.value = '';
|
||||
labelEl.textContent = opts.mode === 'edit' ? (plan.name + '.soon') : '未选择';
|
||||
if (e && e.message === 'invalid_json') soonToast('.soon 须为有效 JSON', 'warn');
|
||||
else soonToast('文件读取失败', 'error');
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function openCreateModal() {
|
||||
var form = '<div class="soon-admin-form">' + uploadFields(true) + metaFields() +
|
||||
Form().actions('上传', 'adminModalSave') + '</div>';
|
||||
var modalState = { content: null, name: '', type: 1, thumbView: '', _blobUrl: '' };
|
||||
|
||||
Ui().openFormModal('上传模板', form, function () {
|
||||
var nameEl = Ui().el('tplName');
|
||||
var fileEl = Ui().el('tplFile');
|
||||
var name = nameEl.value.trim() || defaultNameFromFile(fileEl);
|
||||
if (!name) {
|
||||
soonToast('请填写名称或选择文件', 'warn');
|
||||
Ui().openFormModal('上传模板', modalHtml({ mode: 'create' }), function () {
|
||||
if (!modalState.content) {
|
||||
soonToast('请先选择 .soon 文件', 'warn');
|
||||
return false;
|
||||
}
|
||||
return readFileText(fileEl).then(function (content) {
|
||||
return Api().postJson('templates', {
|
||||
name: name,
|
||||
content: content,
|
||||
sort_order: parseInt(Ui().el('tplSort').value, 10) || 0,
|
||||
is_active: Ui().el('tplActive').checked ? 1 : 0,
|
||||
}).then(function (r) {
|
||||
if (r) {
|
||||
layer.closeAll();
|
||||
soonToast('已上传', 'success');
|
||||
render();
|
||||
}
|
||||
});
|
||||
}).catch(function (e) {
|
||||
if (e && e.message === 'no_file') soonToast('请选择 .soon 文件', 'warn');
|
||||
else soonToast('文件读取失败', 'error');
|
||||
return Api().postJson('templates', {
|
||||
name: modalState.name,
|
||||
content: modalState.content,
|
||||
sort_order: parseInt(Ui().el('tplSort').value, 10) || 0,
|
||||
is_active: Ui().el('tplActive').checked ? 1 : 0,
|
||||
}).then(function (r) {
|
||||
if (r) {
|
||||
revokeBlob(modalState);
|
||||
layer.closeAll();
|
||||
soonToast('已上传', 'success');
|
||||
render();
|
||||
}
|
||||
});
|
||||
}, { area: ['480px', 'auto'] });
|
||||
}, Object.assign({}, MODAL_OPTS, {
|
||||
success: function () { bindTplModal({ mode: 'create' }, modalState); },
|
||||
}));
|
||||
}
|
||||
|
||||
function openEditModal(plan) {
|
||||
var form = '<div class="soon-admin-form">' + metaFields(plan) + uploadFields(false) +
|
||||
Form().actions('保存', 'adminModalSave') + '</div>';
|
||||
var modalState = { content: null, name: plan.name, type: plan.type, thumbView: '', _blobUrl: '' };
|
||||
|
||||
Ui().openFormModal('编辑模板 · ' + plan.name, form, function () {
|
||||
var name = Ui().el('tplName').value.trim();
|
||||
if (!name) {
|
||||
soonToast('名称不能为空', 'warn');
|
||||
return false;
|
||||
}
|
||||
Ui().openFormModal('编辑模板', modalHtml({ mode: 'edit', plan: plan }), function () {
|
||||
var body = {
|
||||
name: name,
|
||||
name: modalState.content ? modalState.name : plan.name,
|
||||
sort_order: parseInt(Ui().el('tplSort').value, 10) || 0,
|
||||
is_active: Ui().el('tplActive').checked ? 1 : 0,
|
||||
};
|
||||
var fileEl = Ui().el('tplFile');
|
||||
var hasFile = fileEl && fileEl.files && fileEl.files[0];
|
||||
var save = function (content) {
|
||||
if (content) body.content = content;
|
||||
return Api().putJson('templates/' + plan.id, body).then(function (r) {
|
||||
if (r) {
|
||||
layer.closeAll();
|
||||
soonToast('已保存', 'success');
|
||||
render();
|
||||
}
|
||||
});
|
||||
};
|
||||
if (hasFile) {
|
||||
return readFileText(fileEl).then(save).catch(function () {
|
||||
soonToast('文件读取失败', 'error');
|
||||
});
|
||||
}
|
||||
return save(null);
|
||||
}, { area: ['480px', 'auto'] });
|
||||
if (modalState.content) body.content = modalState.content;
|
||||
return Api().putJson('templates/' + plan.id, body).then(function (r) {
|
||||
if (r) {
|
||||
revokeBlob(modalState);
|
||||
layer.closeAll();
|
||||
soonToast('已保存', 'success');
|
||||
render();
|
||||
}
|
||||
});
|
||||
}, Object.assign({}, MODAL_OPTS, {
|
||||
success: function () { bindTplModal({ mode: 'edit', plan: plan }, modalState); },
|
||||
}));
|
||||
}
|
||||
|
||||
function deleteTemplate(plan) {
|
||||
@@ -148,7 +332,7 @@
|
||||
var panel = Ui().el('panel-templates');
|
||||
if (!panel) return;
|
||||
panel.innerHTML = '<div class="soon-admin-page">' +
|
||||
Ui().pageHeader('模板库', '管理首页展示的 .soon 模板(缩略图与名称轻量加载)',
|
||||
Ui().pageHeader('模板库', '上传 .soon 后自动识别名称、类型与缩略图',
|
||||
'<button type="button" class="soon-btn soon-btn--sm" id="tplCreate">上传模板</button>') +
|
||||
Ui().skeletonTable() + '</div>';
|
||||
|
||||
@@ -165,7 +349,7 @@
|
||||
}).join('');
|
||||
|
||||
panel.innerHTML = '<div class="soon-admin-page">' +
|
||||
Ui().pageHeader('模板库', '管理首页展示的 .soon 模板(缩略图与名称轻量加载)',
|
||||
Ui().pageHeader('模板库', '上传 .soon 后自动识别名称、类型与缩略图',
|
||||
'<button type="button" class="soon-btn soon-btn--sm" id="tplCreate">上传模板</button>') +
|
||||
Ui().dataTable(
|
||||
['ID', '名称', '类型', '大小', '排序', '缩略图', '状态', '操作'],
|
||||
|
||||
@@ -278,7 +278,7 @@
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body class="soon-design-page">
|
||||
<body class="soon-design-page soon-design--type1">
|
||||
|
||||
<div id="portal-topbar"></div>
|
||||
|
||||
|
||||
@@ -78,7 +78,7 @@
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body class="soon-design-page">
|
||||
<body class="soon-design-page soon-design--type2">
|
||||
|
||||
<div id="portal-topbar"></div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user