@musnows/scriverse 0.4.7 → 0.4.9
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 +5 -2
- package/dist/app.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 +445 -34
- package/dist/public/avatar-crop.d.ts +57 -0
- package/dist/public/avatar-crop.js +201 -0
- package/dist/public/index.html +37 -5
- package/dist/public/styles.css +90 -4
- package/dist/store.js +5 -2
- 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/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
|
+
}
|
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-header-scale">
|
|
14
14
|
</head>
|
|
15
15
|
<body class="auth-pending">
|
|
16
16
|
<section id="auth-view" class="auth-view hidden" aria-labelledby="auth-title">
|
|
@@ -123,10 +123,10 @@
|
|
|
123
123
|
<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>
|
|
124
124
|
<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>
|
|
125
125
|
<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>
|
|
126
|
-
<button type="button" data-module="
|
|
126
|
+
<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>
|
|
127
127
|
<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>
|
|
128
128
|
<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>
|
|
129
|
-
<button class="module-nav-secondary hidden" type="button" data-module="
|
|
129
|
+
<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>
|
|
130
130
|
<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>
|
|
131
131
|
<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>
|
|
132
132
|
<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>
|
|
@@ -202,6 +202,7 @@
|
|
|
202
202
|
<span id="chapter-stats" class="chapter-stats">0 字 · v0</span>
|
|
203
203
|
<button id="insight-button" class="ghost-button" type="button">章节概览</button>
|
|
204
204
|
<button id="versions-button" class="ghost-button" type="button">版本</button>
|
|
205
|
+
<button id="chapter-edit-button" class="primary-button hidden" type="button">编辑</button>
|
|
205
206
|
<button id="tidy-blank-lines-button" class="ghost-button" type="button">整理空行</button>
|
|
206
207
|
<button id="save-button" class="primary-button" type="button">保存正文</button>
|
|
207
208
|
</div>
|
|
@@ -590,7 +591,7 @@
|
|
|
590
591
|
<header class="account-settings-section-header"><h3 id="profile-settings-title">个人资料</h3><p>用于作品协作和历史记录中的作者标识。</p></header>
|
|
591
592
|
<div class="avatar-settings">
|
|
592
593
|
<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
|
|
594
|
+
<div class="avatar-settings-copy"><strong>头像图片</strong><small>支持 PNG、JPEG、WebP,文件不超过 5 MB。选择后可框选正方形选区再裁剪上传。</small></div>
|
|
594
595
|
<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
596
|
</div>
|
|
596
597
|
<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 +614,37 @@
|
|
|
613
614
|
</div>
|
|
614
615
|
</dialog>
|
|
615
616
|
|
|
617
|
+
<dialog id="avatar-crop-dialog" class="dialog avatar-crop-dialog" aria-labelledby="avatar-crop-dialog-title" aria-describedby="avatar-crop-dialog-description">
|
|
618
|
+
<div class="dialog-header">
|
|
619
|
+
<div>
|
|
620
|
+
<span class="eyebrow">个人账户</span>
|
|
621
|
+
<h2 id="avatar-crop-dialog-title">裁剪头像</h2>
|
|
622
|
+
<p id="avatar-crop-dialog-description" class="dialog-header-meta">拖动选区或四角手柄调整范围,圆形预览即最终显示效果。</p>
|
|
623
|
+
</div>
|
|
624
|
+
<button id="avatar-crop-close" class="dialog-close" aria-label="关闭裁剪" type="button">×</button>
|
|
625
|
+
</div>
|
|
626
|
+
<div class="avatar-crop-body">
|
|
627
|
+
<div id="avatar-crop-stage" class="avatar-crop-stage" tabindex="0" role="img" aria-label="头像裁剪选区">
|
|
628
|
+
<img id="avatar-crop-image" class="avatar-crop-image" alt="">
|
|
629
|
+
<div id="avatar-crop-shade" class="avatar-crop-shade" aria-hidden="true"></div>
|
|
630
|
+
<div id="avatar-crop-selection" class="avatar-crop-selection" hidden>
|
|
631
|
+
<button type="button" class="avatar-crop-handle" data-avatar-crop-handle="nw" aria-label="调整左上角"></button>
|
|
632
|
+
<button type="button" class="avatar-crop-handle" data-avatar-crop-handle="ne" aria-label="调整右上角"></button>
|
|
633
|
+
<button type="button" class="avatar-crop-handle" data-avatar-crop-handle="sw" aria-label="调整左下角"></button>
|
|
634
|
+
<button type="button" class="avatar-crop-handle" data-avatar-crop-handle="se" aria-label="调整右下角"></button>
|
|
635
|
+
</div>
|
|
636
|
+
</div>
|
|
637
|
+
<aside class="avatar-crop-side">
|
|
638
|
+
<div id="avatar-crop-preview" class="user-avatar avatar-crop-preview" role="img" aria-label="裁剪预览"></div>
|
|
639
|
+
<p>预览</p>
|
|
640
|
+
</aside>
|
|
641
|
+
</div>
|
|
642
|
+
<div class="dialog-actions">
|
|
643
|
+
<button id="avatar-crop-cancel" class="ghost-button" type="button">取消</button>
|
|
644
|
+
<button id="avatar-crop-confirm" class="primary-button" type="button">确认裁剪</button>
|
|
645
|
+
</div>
|
|
646
|
+
</dialog>
|
|
647
|
+
|
|
616
648
|
<dialog id="members-dialog" class="dialog wide-dialog" aria-labelledby="members-dialog-title">
|
|
617
649
|
<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
650
|
<div class="access-dialog-body">
|
|
@@ -699,6 +731,6 @@
|
|
|
699
731
|
<div id="auth-loading" class="auth-loading" role="status" aria-label="正在载入工作台"></div>
|
|
700
732
|
<script id="vditorIconScript" src="/vendor/vditor/dist/js/icons/ant.js?v=3.11.2"></script>
|
|
701
733
|
<script src="/vendor/vditor/dist/index.min.js?v=3.11.2"></script>
|
|
702
|
-
<script type="module" src="/app.js?v=20260725-
|
|
734
|
+
<script type="module" src="/app.js?v=20260725-module-header-scale"></script>
|
|
703
735
|
</body>
|
|
704
736
|
</html>
|
package/dist/public/styles.css
CHANGED
|
@@ -619,17 +619,23 @@ body.is-panel-resizing { cursor: col-resize; user-select: none; }
|
|
|
619
619
|
.app-shell.prose-hidden-mode:not(.shelf-mode) .panel-heading,
|
|
620
620
|
.app-shell.prose-hidden-mode:not(.shelf-mode) #novel-tree { display: none !important; }
|
|
621
621
|
.app-shell.prose-read-only-mode:not(.shelf-mode) #tidy-blank-lines-button,
|
|
622
|
-
.app-shell.prose-read-only-mode:not(.shelf-mode) #save-button
|
|
622
|
+
.app-shell.prose-read-only-mode:not(.shelf-mode) #save-button,
|
|
623
|
+
.app-shell.prose-read-only-mode:not(.shelf-mode) #chapter-edit-button { display: none !important; }
|
|
623
624
|
.app-shell.prose-read-only-mode:not(.shelf-mode) .left-primary-actions { justify-content: flex-end; }
|
|
624
625
|
.app-shell.prose-read-only-mode:not(.shelf-mode) #chapter-title[readonly],
|
|
625
626
|
.app-shell.prose-read-only-mode:not(.shelf-mode) #chapter-content[readonly] { cursor: default; }
|
|
626
627
|
.app-shell.view-only-mode:not(.shelf-mode) #module-create-button,
|
|
627
628
|
.app-shell.view-only-mode:not(.shelf-mode) [data-work-settings],
|
|
628
629
|
.app-shell.view-only-mode:not(.shelf-mode) #tidy-blank-lines-button,
|
|
629
|
-
.app-shell.view-only-mode:not(.shelf-mode) #save-button
|
|
630
|
+
.app-shell.view-only-mode:not(.shelf-mode) #save-button,
|
|
631
|
+
.app-shell.view-only-mode:not(.shelf-mode) #chapter-edit-button { display: none !important; }
|
|
630
632
|
.app-shell.view-only-mode:not(.shelf-mode) .left-primary-actions { justify-content: flex-end; }
|
|
631
633
|
.app-shell.view-only-mode:not(.shelf-mode) #chapter-title[readonly],
|
|
632
634
|
.app-shell.view-only-mode:not(.shelf-mode) #chapter-content[readonly] { cursor: default; }
|
|
635
|
+
.editor-view.is-read-only #tidy-blank-lines-button,
|
|
636
|
+
.editor-view.is-read-only #save-button { display: none !important; }
|
|
637
|
+
.editor-view.is-read-only #chapter-title[readonly],
|
|
638
|
+
.editor-view.is-read-only #chapter-content[readonly] { cursor: default; }
|
|
633
639
|
body.work-viewer-mode [data-setting-status],
|
|
634
640
|
body.work-viewer-mode [data-edit-setting],
|
|
635
641
|
body.work-viewer-mode [data-edit-character],
|
|
@@ -747,6 +753,74 @@ body.work-viewer-mode .character-editor-actions { display: none !important; }
|
|
|
747
753
|
.avatar-settings-actions { display: flex; flex-wrap: wrap; gap: 8px; }
|
|
748
754
|
.avatar-settings-actions button { min-height: 32px; padding: 6px 11px; font-size: 11px; }
|
|
749
755
|
.avatar-settings-actions button:disabled { cursor: wait; opacity: .5; }
|
|
756
|
+
.avatar-crop-dialog { width: min(720px, 94vw); }
|
|
757
|
+
.avatar-crop-body { display: grid; grid-template-columns: minmax(0, 1fr) 96px; gap: 16px; align-items: start; padding: 18px 24px 8px; }
|
|
758
|
+
.avatar-crop-stage {
|
|
759
|
+
position: relative;
|
|
760
|
+
display: grid;
|
|
761
|
+
place-items: center;
|
|
762
|
+
width: 100%;
|
|
763
|
+
aspect-ratio: 1;
|
|
764
|
+
max-height: min(52vh, 480px);
|
|
765
|
+
overflow: hidden;
|
|
766
|
+
border: 1px solid var(--line);
|
|
767
|
+
border-radius: 6px;
|
|
768
|
+
background:
|
|
769
|
+
linear-gradient(45deg, color-mix(in srgb, var(--line) 55%, transparent) 25%, transparent 25%) 0 0 / 16px 16px,
|
|
770
|
+
linear-gradient(-45deg, color-mix(in srgb, var(--line) 55%, transparent) 25%, transparent 25%) 0 8px / 16px 16px,
|
|
771
|
+
var(--surface-soft);
|
|
772
|
+
touch-action: none;
|
|
773
|
+
user-select: none;
|
|
774
|
+
}
|
|
775
|
+
.avatar-crop-image {
|
|
776
|
+
position: absolute;
|
|
777
|
+
inset: 0;
|
|
778
|
+
width: 100%;
|
|
779
|
+
height: 100%;
|
|
780
|
+
object-fit: contain;
|
|
781
|
+
pointer-events: none;
|
|
782
|
+
}
|
|
783
|
+
.avatar-crop-shade {
|
|
784
|
+
position: absolute;
|
|
785
|
+
inset: 0;
|
|
786
|
+
pointer-events: none;
|
|
787
|
+
background: color-mix(in srgb, var(--ink) 42%, transparent);
|
|
788
|
+
-webkit-mask-image: linear-gradient(#000, #000), linear-gradient(#000, #000);
|
|
789
|
+
mask-image: linear-gradient(#000, #000), linear-gradient(#000, #000);
|
|
790
|
+
-webkit-mask-repeat: no-repeat;
|
|
791
|
+
mask-repeat: no-repeat;
|
|
792
|
+
-webkit-mask-composite: xor;
|
|
793
|
+
mask-composite: exclude;
|
|
794
|
+
}
|
|
795
|
+
.avatar-crop-selection {
|
|
796
|
+
position: absolute;
|
|
797
|
+
box-sizing: border-box;
|
|
798
|
+
border: 2px solid color-mix(in srgb, white 88%, var(--accent));
|
|
799
|
+
border-radius: 2px;
|
|
800
|
+
box-shadow: 0 0 0 1px color-mix(in srgb, var(--ink) 35%, transparent);
|
|
801
|
+
cursor: grab;
|
|
802
|
+
touch-action: none;
|
|
803
|
+
}
|
|
804
|
+
.avatar-crop-selection.is-dragging { cursor: grabbing; }
|
|
805
|
+
.avatar-crop-selection[hidden] { display: none; }
|
|
806
|
+
.avatar-crop-handle {
|
|
807
|
+
position: absolute;
|
|
808
|
+
width: 12px;
|
|
809
|
+
height: 12px;
|
|
810
|
+
padding: 0;
|
|
811
|
+
border: 2px solid color-mix(in srgb, white 90%, var(--accent));
|
|
812
|
+
border-radius: 2px;
|
|
813
|
+
background: var(--accent);
|
|
814
|
+
box-shadow: 0 0 0 1px color-mix(in srgb, var(--ink) 28%, transparent);
|
|
815
|
+
}
|
|
816
|
+
.avatar-crop-handle[data-avatar-crop-handle="nw"] { top: -7px; left: -7px; cursor: nwse-resize; }
|
|
817
|
+
.avatar-crop-handle[data-avatar-crop-handle="ne"] { top: -7px; right: -7px; cursor: nesw-resize; }
|
|
818
|
+
.avatar-crop-handle[data-avatar-crop-handle="sw"] { bottom: -7px; left: -7px; cursor: nesw-resize; }
|
|
819
|
+
.avatar-crop-handle[data-avatar-crop-handle="se"] { bottom: -7px; right: -7px; cursor: nwse-resize; }
|
|
820
|
+
.avatar-crop-side { display: grid; justify-items: center; gap: 8px; padding-top: 8px; }
|
|
821
|
+
.avatar-crop-side p { margin: 0; color: var(--muted); font-size: 10px; }
|
|
822
|
+
.avatar-crop-preview { width: 76px; height: 76px; border: 2px solid color-mix(in srgb, var(--line) 72%, transparent); background: var(--surface); }
|
|
823
|
+
.avatar-crop-preview canvas { position: absolute; inset: 0; width: 100%; height: 100%; border-radius: 50%; }
|
|
750
824
|
.account-settings-form { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: end; gap: 12px; margin-top: 16px; }
|
|
751
825
|
.account-settings-form label { display: grid; min-width: 0; gap: 6px; color: var(--muted); font-size: 11px; }
|
|
752
826
|
.account-settings-form input { width: 100%; min-height: 38px; padding: 9px 10px; background: var(--surface); font-size: 12px; }
|
|
@@ -945,7 +1019,8 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
|
|
|
945
1019
|
.module-header-actions { display: flex; align-items: center; justify-content: flex-end; gap: 8px; flex-wrap: wrap; }
|
|
946
1020
|
.module-header-actions > [data-module-header-action] { order: 1; }
|
|
947
1021
|
.module-header-actions > #module-create-button { order: 2; min-height: 36px; }
|
|
948
|
-
.module-
|
|
1022
|
+
.module-count-badge { display: inline-grid; min-width: 28px; height: 28px; padding: 0 7px; place-items: center; border: 1px solid var(--line); border-radius: 14px; background: var(--surface); color: var(--accent-dark); font-family: var(--font-latin), monospace; font-size: 11px; font-weight: 650; }
|
|
1023
|
+
.module-header h1 { margin: 0 0 6px; font-weight: 500; font-size: 24px; line-height: 1.25; }
|
|
949
1024
|
.module-header p { margin: 0; color: var(--muted); font-size: 13px; }
|
|
950
1025
|
.module-content { padding: 24px 0 80px; }
|
|
951
1026
|
.card-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 13px; }
|
|
@@ -963,6 +1038,8 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
|
|
|
963
1038
|
.module-filter-toggle svg { width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-linecap: round; stroke-linejoin: round; stroke-width: 1.7; }
|
|
964
1039
|
.module-filter-toggle + .module-layout-toolbar { order: 2; }
|
|
965
1040
|
.module-filter-toggle ~ #module-create-button { order: 3; }
|
|
1041
|
+
.race-tree-expand-toolbar ~ .module-layout-toolbar { order: 2; }
|
|
1042
|
+
.race-tree-expand-toolbar ~ #module-create-button { order: 3; }
|
|
966
1043
|
.settings-layout-hint { color: var(--muted); font-size: 10px; }
|
|
967
1044
|
.settings-layout-toggle, .module-layout-toggle { display: inline-flex; border: 1px solid var(--line); border-radius: 4px; overflow: hidden; background: var(--surface); }
|
|
968
1045
|
.settings-layout-toggle button { min-height: 34px; padding: 0 12px; border: 0; border-right: 1px solid var(--line); background: transparent; color: var(--muted); font-size: 11px; }
|
|
@@ -1407,6 +1484,12 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
|
|
|
1407
1484
|
.book-summary-context-percent-field { width: min(100%, 280px); }
|
|
1408
1485
|
.context-compact-threshold-field { width: min(100%, 280px); }
|
|
1409
1486
|
.book-summary-context-percent-field input, .context-compact-threshold-field input { width: 64px; min-height: 32px; padding: 5px 8px; font-size: 13px; font-family: var(--font-latin), monospace; }
|
|
1487
|
+
.config-inline-save { display: flex; align-items: flex-end; gap: 10px; margin: 14px 0 4px; flex-wrap: wrap; }
|
|
1488
|
+
.config-inline-save .book-summary-context-percent-field,
|
|
1489
|
+
.config-inline-save .context-compact-threshold-field { display: grid; gap: 6px; width: auto; margin: 0; color: var(--muted); font-size: 10px; }
|
|
1490
|
+
.config-section .config-save-button { min-height: 32px; padding: 5px 11px; font-size: 11px; }
|
|
1491
|
+
.config-section .card-actions { margin-top: 12px; }
|
|
1492
|
+
.config-section .card-actions .config-save-button { border: 1px solid var(--line); background: transparent; color: var(--ink); }
|
|
1410
1493
|
.field-label textarea {
|
|
1411
1494
|
width: 100%;
|
|
1412
1495
|
min-height: 140px;
|
|
@@ -2038,6 +2121,9 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
|
|
|
2038
2121
|
.account-settings-form, .password-settings-form, .api-key-settings, .api-key-result { grid-template-columns: minmax(0, 1fr); }
|
|
2039
2122
|
.avatar-settings { grid-template-columns: 64px minmax(0, 1fr); }
|
|
2040
2123
|
.profile-avatar-preview { width: 64px; height: 64px; }
|
|
2124
|
+
.avatar-crop-body { grid-template-columns: minmax(0, 1fr); gap: 12px; padding: 14px 16px 4px; }
|
|
2125
|
+
.avatar-crop-side { grid-template-columns: auto 1fr; align-items: center; justify-items: start; gap: 10px; padding-top: 0; }
|
|
2126
|
+
.avatar-crop-preview { width: 64px; height: 64px; }
|
|
2041
2127
|
.account-settings-form .primary-button { width: 100%; }
|
|
2042
2128
|
}
|
|
2043
2129
|
|
|
@@ -2201,7 +2287,7 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
|
|
|
2201
2287
|
|
|
2202
2288
|
.module-view { padding: 26px var(--mobile-gutter) 46px; }
|
|
2203
2289
|
.module-header { padding-bottom: 18px; }
|
|
2204
|
-
.module-header h1 { font-size:
|
|
2290
|
+
.module-header h1 { font-size: 22px; }
|
|
2205
2291
|
.module-header-actions { justify-content: stretch; }
|
|
2206
2292
|
.module-header-actions > * { flex: 1 1 auto; }
|
|
2207
2293
|
.module-content { padding: 18px 0 56px; }
|
package/dist/store.js
CHANGED
|
@@ -608,7 +608,9 @@ export class Store {
|
|
|
608
608
|
autoRunBatchLimit: Math.min(200, Math.max(1, Number(row?.auto_run_batch_limit ?? 20) || 20)),
|
|
609
609
|
bookSummaryContextPercent: Math.min(90, Math.max(1, Number(row?.book_summary_context_percent ?? 50) || 50)),
|
|
610
610
|
contextCompactThreshold: Math.min(90, Math.max(50, Number(row?.context_compact_threshold ?? 85) || 85)),
|
|
611
|
-
agentTools: json(String(row?.agent_tools_json ?? '["story_index","read_chapters","
|
|
611
|
+
agentTools: json(String(row?.agent_tools_json ?? '["story_index","read_chapters","search_story_entities","grep","read_character_sections"]'), ["story_index", "read_chapters", "search_story_entities", "grep", "read_character_sections"])
|
|
612
|
+
.map((tool) => tool === "query_story_knowledge" ? "search_story_entities" : tool)
|
|
613
|
+
.filter((tool, index, tools) => tools.indexOf(tool) === index),
|
|
612
614
|
updatedAt: String(row?.updated_at ?? "")
|
|
613
615
|
};
|
|
614
616
|
}
|
|
@@ -2282,8 +2284,9 @@ export class Store {
|
|
|
2282
2284
|
listCharactersPage(workId, pagination, includeProfileSections = false, includeMerged = false, includeRaceMarkdown = true) {
|
|
2283
2285
|
this.getWork(workId);
|
|
2284
2286
|
const page = paginationSql(pagination);
|
|
2287
|
+
const count = this.db.get(`SELECT COUNT(*) AS count FROM characters WHERE work_id = ?${includeMerged ? "" : " AND merged_into_character_id IS NULL"}`, workId);
|
|
2285
2288
|
const rows = this.db.all(`SELECT * FROM characters WHERE work_id = ?${includeMerged ? "" : " AND merged_into_character_id IS NULL"} ORDER BY name${page.sql}`, workId, ...page.params);
|
|
2286
|
-
return paginated(rows.map((row) => this.mapCharacter(row, includeProfileSections, includeRaceMarkdown)), pagination);
|
|
2289
|
+
return paginated(rows.map((row) => this.mapCharacter(row, includeProfileSections, includeRaceMarkdown)), pagination, Number(count?.count ?? 0));
|
|
2287
2290
|
}
|
|
2288
2291
|
mapCharacterProfileSection(row) {
|
|
2289
2292
|
return {
|