@musnows/scriverse 0.4.7 → 0.4.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ai.js +33 -15
- package/dist/ai.js.map +1 -1
- package/dist/app.js +117 -24
- package/dist/app.js.map +1 -1
- package/dist/collaboration-presence.js +77 -2
- package/dist/collaboration-presence.js.map +1 -1
- package/dist/database.js +30 -1
- package/dist/database.js.map +1 -1
- package/dist/pagination.js +3 -2
- package/dist/pagination.js.map +1 -1
- package/dist/public/app.js +513 -43
- package/dist/public/avatar-crop.d.ts +57 -0
- package/dist/public/avatar-crop.js +201 -0
- package/dist/public/display-labels.d.ts +1 -0
- package/dist/public/display-labels.js +8 -0
- package/dist/public/index.html +68 -34
- package/dist/public/markdown.js +16 -0
- package/dist/public/styles.css +110 -13
- package/dist/store.js +8 -5
- package/dist/store.js.map +1 -1
- package/dist/user-auth.js +12 -2
- package/dist/user-auth.js.map +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
export type CropRect = {
|
|
2
|
+
x: number;
|
|
3
|
+
y: number;
|
|
4
|
+
size: number;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export type DisplayRect = {
|
|
8
|
+
x: number;
|
|
9
|
+
y: number;
|
|
10
|
+
width: number;
|
|
11
|
+
height: number;
|
|
12
|
+
scale: number;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type CropHandle = "nw" | "ne" | "sw" | "se";
|
|
16
|
+
|
|
17
|
+
export const AVATAR_CROP_OUTPUT_SIZE: number;
|
|
18
|
+
export const AVATAR_CROP_MIN_SIZE: number;
|
|
19
|
+
|
|
20
|
+
export function defaultCropRect(imageWidth: number, imageHeight: number): CropRect;
|
|
21
|
+
export function clampCropRect(
|
|
22
|
+
rect: Partial<CropRect> | null | undefined,
|
|
23
|
+
imageWidth: number,
|
|
24
|
+
imageHeight: number,
|
|
25
|
+
minSize?: number
|
|
26
|
+
): CropRect;
|
|
27
|
+
export function moveCropRect(
|
|
28
|
+
rect: Partial<CropRect> | null | undefined,
|
|
29
|
+
deltaX: number,
|
|
30
|
+
deltaY: number,
|
|
31
|
+
imageWidth: number,
|
|
32
|
+
imageHeight: number
|
|
33
|
+
): CropRect;
|
|
34
|
+
export function resizeCropRect(
|
|
35
|
+
rect: Partial<CropRect> | null | undefined,
|
|
36
|
+
handle: CropHandle | string,
|
|
37
|
+
pointerX: number,
|
|
38
|
+
pointerY: number,
|
|
39
|
+
imageWidth: number,
|
|
40
|
+
imageHeight: number
|
|
41
|
+
): CropRect;
|
|
42
|
+
export function containImageRect(
|
|
43
|
+
imageWidth: number,
|
|
44
|
+
imageHeight: number,
|
|
45
|
+
viewportWidth: number,
|
|
46
|
+
viewportHeight: number
|
|
47
|
+
): DisplayRect;
|
|
48
|
+
export function mapDisplayPointToImage(
|
|
49
|
+
point: { x: number; y: number },
|
|
50
|
+
displayRect: DisplayRect
|
|
51
|
+
): { x: number; y: number };
|
|
52
|
+
export function mapImageRectToDisplay(cropRect: CropRect, displayRect: DisplayRect): {
|
|
53
|
+
x: number;
|
|
54
|
+
y: number;
|
|
55
|
+
size: number;
|
|
56
|
+
};
|
|
57
|
+
export function cropOutputSize(cropSize: number, maxSize?: number): number;
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/** 头像裁剪输出边长上限(像素) */
|
|
2
|
+
export const AVATAR_CROP_OUTPUT_SIZE = 512;
|
|
3
|
+
|
|
4
|
+
/** 选区最小边长(原图像素) */
|
|
5
|
+
export const AVATAR_CROP_MIN_SIZE = 32;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @typedef {{ x: number, y: number, size: number }} CropRect
|
|
9
|
+
* @typedef {{ x: number, y: number, width: number, height: number, scale: number }} DisplayRect
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
function finitePositive(value) {
|
|
13
|
+
return Number.isFinite(value) && value > 0 ? value : 0;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 在图片内取居中最大正方形作为默认选区。
|
|
18
|
+
* @param {number} imageWidth
|
|
19
|
+
* @param {number} imageHeight
|
|
20
|
+
* @returns {CropRect}
|
|
21
|
+
*/
|
|
22
|
+
export function defaultCropRect(imageWidth, imageHeight) {
|
|
23
|
+
const width = Math.max(0, Math.floor(finitePositive(imageWidth)));
|
|
24
|
+
const height = Math.max(0, Math.floor(finitePositive(imageHeight)));
|
|
25
|
+
const size = Math.min(width, height);
|
|
26
|
+
if (size < 1) return { x: 0, y: 0, size: 0 };
|
|
27
|
+
return {
|
|
28
|
+
x: Math.floor((width - size) / 2),
|
|
29
|
+
y: Math.floor((height - size) / 2),
|
|
30
|
+
size
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 将正方形选区钳制在图片范围内。
|
|
36
|
+
* @param {Partial<CropRect>} rect
|
|
37
|
+
* @param {number} imageWidth
|
|
38
|
+
* @param {number} imageHeight
|
|
39
|
+
* @param {number} [minSize]
|
|
40
|
+
* @returns {CropRect}
|
|
41
|
+
*/
|
|
42
|
+
export function clampCropRect(rect, imageWidth, imageHeight, minSize = AVATAR_CROP_MIN_SIZE) {
|
|
43
|
+
const width = Math.max(0, Math.floor(finitePositive(imageWidth)));
|
|
44
|
+
const height = Math.max(0, Math.floor(finitePositive(imageHeight)));
|
|
45
|
+
if (width < 1 || height < 1) return { x: 0, y: 0, size: 0 };
|
|
46
|
+
|
|
47
|
+
const maxSize = Math.min(width, height);
|
|
48
|
+
const floorMin = Math.max(1, Math.min(Math.floor(finitePositive(minSize)) || 1, maxSize));
|
|
49
|
+
let size = Math.floor(finitePositive(rect?.size));
|
|
50
|
+
if (size < floorMin) size = floorMin;
|
|
51
|
+
if (size > maxSize) size = maxSize;
|
|
52
|
+
|
|
53
|
+
let x = Math.floor(Number(rect?.x) || 0);
|
|
54
|
+
let y = Math.floor(Number(rect?.y) || 0);
|
|
55
|
+
if (x < 0) x = 0;
|
|
56
|
+
if (y < 0) y = 0;
|
|
57
|
+
if (x > width - size) x = width - size;
|
|
58
|
+
if (y > height - size) y = height - size;
|
|
59
|
+
return { x, y, size };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* 平移选区。
|
|
64
|
+
* @param {CropRect} rect
|
|
65
|
+
* @param {number} deltaX
|
|
66
|
+
* @param {number} deltaY
|
|
67
|
+
* @param {number} imageWidth
|
|
68
|
+
* @param {number} imageHeight
|
|
69
|
+
* @returns {CropRect}
|
|
70
|
+
*/
|
|
71
|
+
export function moveCropRect(rect, deltaX, deltaY, imageWidth, imageHeight) {
|
|
72
|
+
return clampCropRect({
|
|
73
|
+
x: (Number(rect?.x) || 0) + (Number(deltaX) || 0),
|
|
74
|
+
y: (Number(rect?.y) || 0) + (Number(deltaY) || 0),
|
|
75
|
+
size: rect?.size
|
|
76
|
+
}, imageWidth, imageHeight);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* 从四角手柄调整正方形选区,对边角保持固定。
|
|
81
|
+
* @param {CropRect} rect
|
|
82
|
+
* @param {"nw"|"ne"|"sw"|"se"} handle
|
|
83
|
+
* @param {number} pointerX 原图像素坐标
|
|
84
|
+
* @param {number} pointerY 原图像素坐标
|
|
85
|
+
* @param {number} imageWidth
|
|
86
|
+
* @param {number} imageHeight
|
|
87
|
+
* @returns {CropRect}
|
|
88
|
+
*/
|
|
89
|
+
export function resizeCropRect(rect, handle, pointerX, pointerY, imageWidth, imageHeight) {
|
|
90
|
+
const current = clampCropRect(rect, imageWidth, imageHeight);
|
|
91
|
+
const left = current.x;
|
|
92
|
+
const top = current.y;
|
|
93
|
+
const right = current.x + current.size;
|
|
94
|
+
const bottom = current.y + current.size;
|
|
95
|
+
const px = Number(pointerX) || 0;
|
|
96
|
+
const py = Number(pointerY) || 0;
|
|
97
|
+
|
|
98
|
+
let nextLeft = left;
|
|
99
|
+
let nextTop = top;
|
|
100
|
+
let nextRight = right;
|
|
101
|
+
let nextBottom = bottom;
|
|
102
|
+
|
|
103
|
+
if (handle === "nw") {
|
|
104
|
+
nextLeft = px;
|
|
105
|
+
nextTop = py;
|
|
106
|
+
} else if (handle === "ne") {
|
|
107
|
+
nextRight = px;
|
|
108
|
+
nextTop = py;
|
|
109
|
+
} else if (handle === "sw") {
|
|
110
|
+
nextLeft = px;
|
|
111
|
+
nextBottom = py;
|
|
112
|
+
} else {
|
|
113
|
+
nextRight = px;
|
|
114
|
+
nextBottom = py;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const widthSpan = Math.abs(nextRight - nextLeft);
|
|
118
|
+
const heightSpan = Math.abs(nextBottom - nextTop);
|
|
119
|
+
const size = Math.max(widthSpan, heightSpan);
|
|
120
|
+
|
|
121
|
+
if (handle === "nw") {
|
|
122
|
+
return clampCropRect({ x: right - size, y: bottom - size, size }, imageWidth, imageHeight);
|
|
123
|
+
}
|
|
124
|
+
if (handle === "ne") {
|
|
125
|
+
return clampCropRect({ x: left, y: bottom - size, size }, imageWidth, imageHeight);
|
|
126
|
+
}
|
|
127
|
+
if (handle === "sw") {
|
|
128
|
+
return clampCropRect({ x: right - size, y: top, size }, imageWidth, imageHeight);
|
|
129
|
+
}
|
|
130
|
+
return clampCropRect({ x: left, y: top, size }, imageWidth, imageHeight);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* 计算 object-fit: contain 时图片在视口中的显示矩形。
|
|
135
|
+
* @param {number} imageWidth
|
|
136
|
+
* @param {number} imageHeight
|
|
137
|
+
* @param {number} viewportWidth
|
|
138
|
+
* @param {number} viewportHeight
|
|
139
|
+
* @returns {DisplayRect}
|
|
140
|
+
*/
|
|
141
|
+
export function containImageRect(imageWidth, imageHeight, viewportWidth, viewportHeight) {
|
|
142
|
+
const width = finitePositive(imageWidth);
|
|
143
|
+
const height = finitePositive(imageHeight);
|
|
144
|
+
const viewW = finitePositive(viewportWidth);
|
|
145
|
+
const viewH = finitePositive(viewportHeight);
|
|
146
|
+
if (!width || !height || !viewW || !viewH) {
|
|
147
|
+
return { x: 0, y: 0, width: 0, height: 0, scale: 0 };
|
|
148
|
+
}
|
|
149
|
+
const scale = Math.min(viewW / width, viewH / height);
|
|
150
|
+
const displayWidth = width * scale;
|
|
151
|
+
const displayHeight = height * scale;
|
|
152
|
+
return {
|
|
153
|
+
x: (viewW - displayWidth) / 2,
|
|
154
|
+
y: (viewH - displayHeight) / 2,
|
|
155
|
+
width: displayWidth,
|
|
156
|
+
height: displayHeight,
|
|
157
|
+
scale
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* 视口坐标映射到原图像素坐标。
|
|
163
|
+
* @param {{ x: number, y: number }} point
|
|
164
|
+
* @param {DisplayRect} displayRect
|
|
165
|
+
* @returns {{ x: number, y: number }}
|
|
166
|
+
*/
|
|
167
|
+
export function mapDisplayPointToImage(point, displayRect) {
|
|
168
|
+
const scale = finitePositive(displayRect?.scale);
|
|
169
|
+
if (!scale) return { x: 0, y: 0 };
|
|
170
|
+
return {
|
|
171
|
+
x: ((Number(point?.x) || 0) - (Number(displayRect.x) || 0)) / scale,
|
|
172
|
+
y: ((Number(point?.y) || 0) - (Number(displayRect.y) || 0)) / scale
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* 原图选区映射到视口显示矩形。
|
|
178
|
+
* @param {CropRect} cropRect
|
|
179
|
+
* @param {DisplayRect} displayRect
|
|
180
|
+
* @returns {{ x: number, y: number, size: number }}
|
|
181
|
+
*/
|
|
182
|
+
export function mapImageRectToDisplay(cropRect, displayRect) {
|
|
183
|
+
const scale = finitePositive(displayRect?.scale);
|
|
184
|
+
return {
|
|
185
|
+
x: (Number(displayRect?.x) || 0) + (Number(cropRect?.x) || 0) * scale,
|
|
186
|
+
y: (Number(displayRect?.y) || 0) + (Number(cropRect?.y) || 0) * scale,
|
|
187
|
+
size: (Number(cropRect?.size) || 0) * scale
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* 裁剪输出边长:不超过原选区,也不超过上限。
|
|
193
|
+
* @param {number} cropSize
|
|
194
|
+
* @param {number} [maxSize]
|
|
195
|
+
* @returns {number}
|
|
196
|
+
*/
|
|
197
|
+
export function cropOutputSize(cropSize, maxSize = AVATAR_CROP_OUTPUT_SIZE) {
|
|
198
|
+
const size = Math.max(1, Math.floor(finitePositive(cropSize)) || 1);
|
|
199
|
+
const limit = Math.max(1, Math.floor(finitePositive(maxSize)) || AVATAR_CROP_OUTPUT_SIZE);
|
|
200
|
+
return Math.min(size, limit);
|
|
201
|
+
}
|
|
@@ -13,3 +13,4 @@ export function providerConnectionLabel(value: unknown): string;
|
|
|
13
13
|
export function chapterVersionSourceLabel(value: unknown): string;
|
|
14
14
|
export function occurrenceRoleLabel(value: unknown): string;
|
|
15
15
|
export function searchResultTypeLabel(value: unknown): string;
|
|
16
|
+
export function characterStateFieldLabel(value: unknown): string;
|
|
@@ -75,3 +75,11 @@ export function occurrenceRoleLabel(value) {
|
|
|
75
75
|
export function searchResultTypeLabel(value) {
|
|
76
76
|
return enumLabel({ chapter: "章节", setting: "设定", character: "角色", race: "种族", organization: "组织" }, value, "其他资料");
|
|
77
77
|
}
|
|
78
|
+
|
|
79
|
+
export function characterStateFieldLabel(value) {
|
|
80
|
+
const normalized = String(value ?? "").trim();
|
|
81
|
+
return enumLabel({
|
|
82
|
+
location: "位置",
|
|
83
|
+
condition: "状况"
|
|
84
|
+
}, normalized, normalized || "未命名字段");
|
|
85
|
+
}
|
package/dist/public/index.html
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
<link rel="icon" href="/icon.svg?v=20260712" type="image/svg+xml">
|
|
11
11
|
<link rel="manifest" href="/site.webmanifest">
|
|
12
12
|
<link rel="stylesheet" href="/vendor/vditor/dist/index.css?v=3.11.2">
|
|
13
|
-
<link rel="stylesheet" href="/styles.css?v=20260725-
|
|
13
|
+
<link rel="stylesheet" href="/styles.css?v=20260725-module-count-vditor-left-panel">
|
|
14
14
|
</head>
|
|
15
15
|
<body class="auth-pending">
|
|
16
16
|
<section id="auth-view" class="auth-view hidden" aria-labelledby="auth-title">
|
|
@@ -105,40 +105,42 @@
|
|
|
105
105
|
|
|
106
106
|
<aside class="left-panel">
|
|
107
107
|
<div id="left-panel-resize" class="panel-resize-handle" role="separator" aria-label="调整作品侧栏宽度" aria-orientation="vertical" tabindex="0"></div>
|
|
108
|
-
<div class="left-
|
|
109
|
-
<div class="left-
|
|
110
|
-
<
|
|
111
|
-
<
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
108
|
+
<div class="left-panel-body">
|
|
109
|
+
<div class="left-actions">
|
|
110
|
+
<div class="left-primary-actions">
|
|
111
|
+
<label id="import-file-button" class="file-button" aria-label="导入 TXT / DOCX">
|
|
112
|
+
<span class="import-file-label import-file-label-full" aria-hidden="true">导入 TXT / DOCX</span>
|
|
113
|
+
<span class="import-file-label import-file-label-compact" aria-hidden="true">导入TXT/DOCX</span>
|
|
114
|
+
<span class="import-file-label import-file-label-short" aria-hidden="true">导入</span>
|
|
115
|
+
<input id="import-file" type="file" accept=".txt,.docx">
|
|
116
|
+
</label>
|
|
117
|
+
<button id="left-panel-toggle" class="panel-collapse-button" type="button" aria-label="收起作品侧栏" aria-expanded="true">‹</button>
|
|
118
|
+
</div>
|
|
117
119
|
</div>
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
120
|
+
<nav id="module-nav" class="module-nav" aria-label="作品模块">
|
|
121
|
+
<button type="button" data-module="editor" class="active"><svg class="nav-icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"/><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/></svg>正文</button>
|
|
122
|
+
<button type="button" data-module="settings"><svg class="nav-icon" viewBox="0 0 24 24" aria-hidden="true"><path d="m16 6 4 14"/><path d="M12 6v14"/><path d="M8 8v12"/><path d="M4 4v16"/></svg>设定库</button>
|
|
123
|
+
<button type="button" data-module="characters"><svg class="nav-icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>角色</button>
|
|
124
|
+
<button type="button" data-module="organizations"><svg class="nav-icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>组织</button>
|
|
125
|
+
<button type="button" data-module="timeline"><svg class="nav-icon" viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></svg>时间轴</button>
|
|
126
|
+
<button type="button" data-module="relationships"><svg class="nav-icon" viewBox="0 0 24 24" aria-hidden="true"><circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/><path d="m8.59 13.51 6.83 3.98"/><path d="m15.41 6.51-6.82 3.98"/></svg>关系</button>
|
|
127
|
+
<button type="button" data-module="races"><svg class="nav-icon" viewBox="0 0 24 24" aria-hidden="true"><circle cx="11" cy="4" r="2"/><circle cx="18" cy="8" r="2"/><circle cx="20" cy="16" r="2"/><path d="M9 10a5 5 0 0 1 5 5v3.5a3.5 3.5 0 0 1-6.84 1.045Q6.52 17.48 4.46 16.84A3.5 3.5 0 0 1 5.5 10Z"/></svg>种族</button>
|
|
128
|
+
<button class="ai-analysis-entry" type="button" data-module="tasks"><svg class="nav-icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M9.937 15.5A2 2 0 0 0 8.5 14.063l-6.135-1.582a.5.5 0 0 1 0-.962L8.5 9.936A2 2 0 0 0 9.937 8.5l1.582-6.135a.5.5 0 0 1 .963 0L14.063 8.5A2 2 0 0 0 15.5 9.937l6.135 1.581a.5.5 0 0 1 0 .964L15.5 14.063a2 2 0 0 0-1.437 1.437l-1.582 6.135a.5.5 0 0 1-.963 0z"/><path d="M20 3v4"/><path d="M22 5h-4"/><path d="M4 17v2"/><path d="M5 18H3"/></svg>AI 分析</button>
|
|
129
|
+
<button id="module-more-button" type="button" aria-expanded="false"><svg class="nav-icon" viewBox="0 0 24 24" aria-hidden="true"><path d="m6 9 6 6 6-6"/></svg><span class="nav-label">更多</span></button>
|
|
130
|
+
<button class="module-nav-secondary hidden" type="button" data-module="outlines"><svg class="nav-icon" viewBox="0 0 24 24" aria-hidden="true"><path d="m3 17 2 2 4-4"/><path d="m3 7 2 2 4-4"/><path d="M13 6h8"/><path d="M13 12h8"/><path d="M13 18h8"/></svg><span class="nav-label">大纲/伏笔</span></button>
|
|
131
|
+
<button class="module-nav-secondary hidden" type="button" data-module="reviews"><svg class="nav-icon" viewBox="0 0 24 24" aria-hidden="true"><rect width="8" height="4" x="8" y="2" rx="1" ry="1"/><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"/><path d="m9 14 2 2 4-4"/></svg>审核</button>
|
|
132
|
+
<button class="module-nav-secondary hidden" type="button" data-module="ai-settings"><svg class="nav-icon" viewBox="0 0 24 24" aria-hidden="true"><rect width="16" height="16" x="4" y="4" rx="2"/><rect width="6" height="6" x="9" y="9" rx="1"/><path d="M15 2v2"/><path d="M15 20v2"/><path d="M2 15h2"/><path d="M2 9h2"/><path d="M20 15h2"/><path d="M20 9h2"/><path d="M9 2v2"/><path d="M9 20v2"/></svg>AI 设置</button>
|
|
133
|
+
<button class="module-nav-secondary hidden" type="button" data-work-settings><svg class="nav-icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.09a2 2 0 0 1 1 1.74v.5a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.38a2 2 0 0 0-.73-2.73l-.15-.09a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2Z"/><circle cx="12" cy="12" r="3"/></svg>作品设置</button>
|
|
134
|
+
</nav>
|
|
135
|
+
<div class="panel-heading">
|
|
136
|
+
<span>作品目录</span>
|
|
137
|
+
<div class="panel-heading-actions">
|
|
138
|
+
<span id="chapter-count">0 章</span>
|
|
139
|
+
<button id="new-volume-button" class="add-button" type="button" aria-label="新建分卷" title="新建分卷">+</button>
|
|
140
|
+
</div>
|
|
139
141
|
</div>
|
|
142
|
+
<div id="novel-tree" class="novel-tree empty-copy">创建或选择作品后开始写作</div>
|
|
140
143
|
</div>
|
|
141
|
-
<div id="novel-tree" class="novel-tree empty-copy">创建或选择作品后开始写作</div>
|
|
142
144
|
</aside>
|
|
143
145
|
|
|
144
146
|
<main id="main-panel" class="main-panel">
|
|
@@ -202,6 +204,7 @@
|
|
|
202
204
|
<span id="chapter-stats" class="chapter-stats">0 字 · v0</span>
|
|
203
205
|
<button id="insight-button" class="ghost-button" type="button">章节概览</button>
|
|
204
206
|
<button id="versions-button" class="ghost-button" type="button">版本</button>
|
|
207
|
+
<button id="chapter-edit-button" class="primary-button hidden" type="button">编辑</button>
|
|
205
208
|
<button id="tidy-blank-lines-button" class="ghost-button" type="button">整理空行</button>
|
|
206
209
|
<button id="save-button" class="primary-button" type="button">保存正文</button>
|
|
207
210
|
</div>
|
|
@@ -590,7 +593,7 @@
|
|
|
590
593
|
<header class="account-settings-section-header"><h3 id="profile-settings-title">个人资料</h3><p>用于作品协作和历史记录中的作者标识。</p></header>
|
|
591
594
|
<div class="avatar-settings">
|
|
592
595
|
<div id="profile-avatar-preview" class="user-avatar profile-avatar-preview" role="img" aria-label="当前头像"><span class="user-avatar-fallback">作</span></div>
|
|
593
|
-
<div class="avatar-settings-copy"><strong>头像图片</strong><small>支持 PNG、JPEG、WebP,文件不超过 5 MB
|
|
596
|
+
<div class="avatar-settings-copy"><strong>头像图片</strong><small>支持 PNG、JPEG、WebP,文件不超过 5 MB。选择后可框选正方形选区再裁剪上传。</small></div>
|
|
594
597
|
<div class="avatar-settings-actions"><button id="avatar-upload-button" class="ghost-button" type="button">上传头像</button><button id="avatar-remove-button" class="ghost-button hidden" type="button">移除头像</button></div>
|
|
595
598
|
</div>
|
|
596
599
|
<form id="profile-form" class="account-settings-form"><label>显示名称<input id="profile-display-name" name="displayName" maxlength="80" required></label><button class="primary-button" type="submit">保存名称</button></form>
|
|
@@ -613,6 +616,37 @@
|
|
|
613
616
|
</div>
|
|
614
617
|
</dialog>
|
|
615
618
|
|
|
619
|
+
<dialog id="avatar-crop-dialog" class="dialog avatar-crop-dialog" aria-labelledby="avatar-crop-dialog-title" aria-describedby="avatar-crop-dialog-description">
|
|
620
|
+
<div class="dialog-header">
|
|
621
|
+
<div>
|
|
622
|
+
<span class="eyebrow">个人账户</span>
|
|
623
|
+
<h2 id="avatar-crop-dialog-title">裁剪头像</h2>
|
|
624
|
+
<p id="avatar-crop-dialog-description" class="dialog-header-meta">拖动选区或四角手柄调整范围,圆形预览即最终显示效果。</p>
|
|
625
|
+
</div>
|
|
626
|
+
<button id="avatar-crop-close" class="dialog-close" aria-label="关闭裁剪" type="button">×</button>
|
|
627
|
+
</div>
|
|
628
|
+
<div class="avatar-crop-body">
|
|
629
|
+
<div id="avatar-crop-stage" class="avatar-crop-stage" tabindex="0" role="img" aria-label="头像裁剪选区">
|
|
630
|
+
<img id="avatar-crop-image" class="avatar-crop-image" alt="">
|
|
631
|
+
<div id="avatar-crop-shade" class="avatar-crop-shade" aria-hidden="true"></div>
|
|
632
|
+
<div id="avatar-crop-selection" class="avatar-crop-selection" hidden>
|
|
633
|
+
<button type="button" class="avatar-crop-handle" data-avatar-crop-handle="nw" aria-label="调整左上角"></button>
|
|
634
|
+
<button type="button" class="avatar-crop-handle" data-avatar-crop-handle="ne" aria-label="调整右上角"></button>
|
|
635
|
+
<button type="button" class="avatar-crop-handle" data-avatar-crop-handle="sw" aria-label="调整左下角"></button>
|
|
636
|
+
<button type="button" class="avatar-crop-handle" data-avatar-crop-handle="se" aria-label="调整右下角"></button>
|
|
637
|
+
</div>
|
|
638
|
+
</div>
|
|
639
|
+
<aside class="avatar-crop-side">
|
|
640
|
+
<div id="avatar-crop-preview" class="user-avatar avatar-crop-preview" role="img" aria-label="裁剪预览"></div>
|
|
641
|
+
<p>预览</p>
|
|
642
|
+
</aside>
|
|
643
|
+
</div>
|
|
644
|
+
<div class="dialog-actions">
|
|
645
|
+
<button id="avatar-crop-cancel" class="ghost-button" type="button">取消</button>
|
|
646
|
+
<button id="avatar-crop-confirm" class="primary-button" type="button">确认裁剪</button>
|
|
647
|
+
</div>
|
|
648
|
+
</dialog>
|
|
649
|
+
|
|
616
650
|
<dialog id="members-dialog" class="dialog wide-dialog" aria-labelledby="members-dialog-title">
|
|
617
651
|
<div class="dialog-header"><div><span id="members-dialog-eyebrow" class="eyebrow">作品权限</span><h2 id="members-dialog-title">成员模块权限</h2></div><button id="members-dialog-close" class="dialog-close" aria-label="关闭" type="button">×</button></div>
|
|
618
652
|
<div class="access-dialog-body">
|
|
@@ -699,6 +733,6 @@
|
|
|
699
733
|
<div id="auth-loading" class="auth-loading" role="status" aria-label="正在载入工作台"></div>
|
|
700
734
|
<script id="vditorIconScript" src="/vendor/vditor/dist/js/icons/ant.js?v=3.11.2"></script>
|
|
701
735
|
<script src="/vendor/vditor/dist/index.min.js?v=3.11.2"></script>
|
|
702
|
-
<script type="module" src="/app.js?v=20260725-
|
|
736
|
+
<script type="module" src="/app.js?v=20260725-collab-refresh-left-panel-resize"></script>
|
|
703
737
|
</body>
|
|
704
738
|
</html>
|
package/dist/public/markdown.js
CHANGED
|
@@ -199,6 +199,22 @@ export function renderMarkdown(value) {
|
|
|
199
199
|
}
|
|
200
200
|
flushQuote();
|
|
201
201
|
if (!line.trim()) {
|
|
202
|
+
// CommonMark 松散列表:同类型列表项之间的空行不结束列表,
|
|
203
|
+
// 否则每个条目会各自生成 <ol>/<ul>,有序序号会全部显示为 1。
|
|
204
|
+
if (list) {
|
|
205
|
+
let nextIndex = lineIndex + 1;
|
|
206
|
+
while (nextIndex < lines.length && !lines[nextIndex].trim()) nextIndex += 1;
|
|
207
|
+
if (nextIndex < lines.length) {
|
|
208
|
+
const nextListItem = lines[nextIndex].match(/^(\s*)([-+*]|\d+\.)\s+(.+)$/u);
|
|
209
|
+
if (nextListItem) {
|
|
210
|
+
const nextTag = /\d+\./u.test(nextListItem[2]) ? "ol" : "ul";
|
|
211
|
+
if (nextTag === list.tag) {
|
|
212
|
+
flushParagraph();
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
202
218
|
flushBlocks();
|
|
203
219
|
continue;
|
|
204
220
|
}
|