Skip to content

Commit c9bd7d8

Browse files
authored
Merge pull request #103 from devlive-community/refactor-nodejs
Refactor nodejs
2 parents f6990c5 + 5807ad3 commit c9bd7d8

File tree

8 files changed

+137
-66
lines changed

8 files changed

+137
-66
lines changed

backend/models/book.js

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,15 @@ class Book {
88
* @returns {Promise<number>} 插入的书籍
99
*/
1010
static async create(bookData) {
11-
const { title, description, cover_image, slug, user_id, status = 'draft', is_public = false } = bookData
11+
const { title, description, cover_image, slug, user_id, status = 'draft', is_public = false, order_col = 'created_at', order_dir = 'desc' } = bookData
1212

1313
try {
1414
const pool = getPool()
1515
const connection = await pool.getConnection()
1616

1717
const [result] = await connection.execute(
18-
'INSERT INTO books (title, description, cover_image, slug, user_id, status, is_public) VALUES (?, ?, ?, ?, ?, ?, ?)',
19-
[title, description || null, cover_image || null, slug, user_id, status, is_public]
18+
'INSERT INTO books (title, description, cover_image, slug, user_id, status, is_public, order_col, order_dir) VALUES (?, ?, ?, ?, ?, ?, ?)',
19+
[title, description || null, cover_image || null, slug, user_id, status, is_public, order_col, order_dir]
2020
)
2121
connection.release()
2222

@@ -170,6 +170,8 @@ class Book {
170170
b.status,
171171
b.is_public,
172172
b.view_count,
173+
b.order_col,
174+
b.order_dir,
173175
DATE_FORMAT(b.created_at, '%Y-%m-%d %H:%i:%s') AS created_at,
174176
DATE_FORMAT(b.updated_at, '%Y-%m-%d %H:%i:%s') AS updated_at,
175177
b.user_id,
@@ -442,6 +444,14 @@ class Book {
442444
fields.push('is_public = ?')
443445
values.push(bookData.is_public)
444446
}
447+
if (bookData.order_col !== undefined) {
448+
fields.push('order_col = ?')
449+
values.push(bookData.order_col)
450+
}
451+
if (bookData.order_dir !== undefined) {
452+
fields.push('order_dir = ?')
453+
values.push(bookData.order_dir)
454+
}
445455

446456
if (fields.length === 0) {
447457
throw new Error('没有提供要更新的字段')

backend/models/document.js

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ class Document {
120120
}
121121
}
122122

123-
static async getDocumentTree(searchParams = {}) {
123+
static async getDocumentTree(searchParams = {}, orderConfig = null) {
124124
try {
125125
const whereConditions = []
126126
const params = []
@@ -137,15 +137,15 @@ class Document {
137137

138138
const whereClause = whereConditions.length > 0 ? 'WHERE ' + whereConditions.join(' AND ') : ''
139139

140+
const orderClause = this.buildOrderClause(orderConfig)
141+
140142
const pool = getPool()
141143
const connection = await pool.getConnection()
142144
const [rows] = await connection.execute(`
143145
SELECT *
144-
FROM documents d ${ whereClause }
145-
ORDER BY sort_order ASC, created_at ASC`, params)
146+
FROM documents d ${ whereClause } ${ orderClause }`, params)
146147
connection.release()
147148

148-
// 构建树形结构
149149
const buildTree = (documents, parentId = null) => {
150150
return documents
151151
.filter(doc => doc.parent_id === parentId)
@@ -158,7 +158,7 @@ class Document {
158158
return buildTree(rows)
159159
}
160160
catch (error) {
161-
console.error(`获取文档树失败 ${ bookId }:`, error)
161+
console.error(`获取文档树失败 ${ searchParams.book_id }:`, error)
162162
throw error
163163
}
164164
}
@@ -279,7 +279,7 @@ class Document {
279279
}
280280
}
281281

282-
static async findAllByConditions(searchParams = {}) {
282+
static async findAllByConditions(searchParams = {}, orderConfig = null) {
283283
try {
284284
const whereConditions = []
285285
const params = []
@@ -311,15 +311,16 @@ class Document {
311311

312312
const whereClause = whereConditions.length > 0 ? 'WHERE ' + whereConditions.join(' AND ') : ''
313313

314+
const orderClause = this.buildOrderClause(orderConfig)
315+
314316
const pool = getPool()
315317
const [rows] = await pool.query(`
316318
SELECT d.*,
317319
DATE_FORMAT(d.created_at, '%Y-%m-%d %H:%i:%s') AS created_at,
318320
DATE_FORMAT(d.updated_at, '%Y-%m-%d %H:%i:%s') AS updated_at
319321
FROM documents d
320322
LEFT JOIN users u ON d.user_id = u.id
321-
${ whereClause }
322-
ORDER BY sort_order ASC, created_at ASC`, params)
323+
${ whereClause } ${ orderClause }`, params)
323324

324325
return rows || []
325326
}
@@ -328,6 +329,29 @@ class Document {
328329
}
329330
}
330331

332+
static buildOrderClause(orderConfig) {
333+
if (!orderConfig || !orderConfig.order_col || !orderConfig.order_dir) {
334+
return 'ORDER BY d.sort_order ASC, d.created_at ASC'
335+
}
336+
337+
const { order_col, order_dir } = orderConfig
338+
339+
const allowedColumns = {
340+
'created_at': 'd.created_at',
341+
'updated_at': 'd.updated_at',
342+
'sort_order': 'd.sort_order',
343+
'title': 'd.title'
344+
}
345+
346+
const allowedDirections = ['asc', 'desc']
347+
348+
if (!allowedColumns[order_col] || !allowedDirections.includes(order_dir.toLowerCase())) {
349+
return 'ORDER BY d.sort_order ASC, d.created_at ASC'
350+
}
351+
352+
return `ORDER BY ${ allowedColumns[order_col] } ${ order_dir.toUpperCase() }, d.sort_order ASC`
353+
}
354+
331355
static async findBySlug(slug) {
332356
try {
333357
const pool = getPool()

backend/routes/book.js

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ router.get('/create', ensureAuthenticated, asyncHandler(async (req, res) => {
1616

1717
router.post('/create', ensureAuthenticated, asyncHandler(async (req, res) => {
1818
try {
19-
const { title, slug, description, status, is_public } = req.body
19+
const { title, slug, description, status, is_public, order_col, order_dir } = req.body
2020
const user_id = req.user.id
2121

2222
if (!title || !slug) {
@@ -41,7 +41,7 @@ router.post('/create', ensureAuthenticated, asyncHandler(async (req, res) => {
4141
return res.render('pages/book/info', { isEdit: false, error: `URL 路径 ${ slug } 已存在,请使用其他路径` })
4242
}
4343

44-
const book = await Book.create({ title, description, slug, user_id, status, is_public })
44+
const book = await Book.create({ title, description, slug, user_id, status, is_public, order_col, order_dir })
4545

4646
const coverFile = req.files?.find(file => file.fieldname === 'cover_image')
4747
if (coverFile) {
@@ -89,7 +89,9 @@ router.get('/reader/:username/:book_slug/:docs_slug?', asyncHandler(async (req,
8989
}
9090

9191
const searchParams = { book_id: book.id, status: 'published' }
92-
const documents = await Document.getDocumentTree(searchParams)
92+
const orderConfig = { order_col: book.order_col, order_dir: book.order_dir }
93+
94+
const documents = await Document.getDocumentTree(searchParams, orderConfig)
9395
if (documents.length === 0) {
9496
return res.status(404).render('pages/error/global', {
9597
error: {
@@ -259,7 +261,11 @@ router.get('/:username/:slug/chapters', asyncHandler(async (req, res) => {
259261
const searchParams = { parent_id: null, book_id: book.id }
260262
if (book.user_id !== req.user?.id) { searchParams.status = 'published' }
261263

262-
const data = await Document.findAllByConditions(searchParams)
264+
const orderConfig = {
265+
order_col: book.order_col,
266+
order_dir: book.order_dir
267+
}
268+
const data = await Document.findAllByConditions(searchParams, orderConfig)
263269

264270
res.render('pages/book/chapters', { book, data })
265271
}))
@@ -303,7 +309,7 @@ router.put('/:username/:slug/edit', ensureAuthenticated, asyncHandler(async (req
303309
})
304310
}
305311

306-
const { title, slug, description, status, is_public, remove_cover } = req.body
312+
const { title, slug, description, status, is_public, remove_cover, order_col, order_dir } = req.body
307313

308314
if (!title || !slug) {
309315
return res.render('pages/book/info', { isEdit: true, book, error: '标题和 URL 路径是必填项' })
@@ -332,7 +338,9 @@ router.put('/:username/:slug/edit', ensureAuthenticated, asyncHandler(async (req
332338
slug,
333339
description,
334340
status: status || 'draft',
335-
is_public: is_public === '1'
341+
is_public: is_public === '1',
342+
order_col: order_col || 'created_at',
343+
order_dir: order_dir || 'asc'
336344
}
337345

338346
const coverFile = req.files?.find(file => file.fieldname === 'cover_image')

backend/scripts/schema.sql

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -61,11 +61,11 @@ CREATE TABLE IF NOT EXISTS books
6161
cover_image VARCHAR(500) COMMENT '封面图片URL',
6262
slug VARCHAR(255) NOT NULL COMMENT 'URL路径标识符',
6363
user_id INT NOT NULL COMMENT '作者ID',
64-
status ENUM ('draft', 'published', 'archived') DEFAULT 'draft' COMMENT '状态',
65-
is_public BOOLEAN DEFAULT FALSE COMMENT '是否公开',
66-
view_count INT DEFAULT 0 COMMENT '浏览次数',
67-
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
68-
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
64+
status ENUM ('draft', 'published', 'archived') DEFAULT 'draft' COMMENT '状态',
65+
is_public BOOLEAN DEFAULT FALSE COMMENT '是否公开',
66+
view_count INT DEFAULT 0 COMMENT '浏览次数',
67+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
68+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
6969
UNIQUE KEY unique_slug (slug),
7070
INDEX idx_user_id (user_id),
7171
INDEX idx_status (status),

frontend/views/components/tree.ejs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
* - document: 文档图标
2020
* @param {String} linkPattern - 链接模式,默认'/document/writer/{username}/{bookSlug}/{docSlug}'
2121
* 可用占位符:{username}, {bookSlug}, {docSlug}, {docId}
22+
* @param {Boolean} showPath - 是否显示路径,默认 true
2223
*/
2324
2425
const treeDocuments = typeof documents !== 'undefined' ? documents : [];
@@ -34,6 +35,7 @@ const treeShowStatusBadge = typeof showStatusBadge !== 'undefined' ? showStatusB
3435
const treeStatusField = typeof statusField !== 'undefined' ? statusField : 'status';
3536
const treeShowIcons = typeof showIcons !== 'undefined' ? showIcons : true;
3637
const treeLinkPattern = typeof linkPattern !== 'undefined' ? linkPattern : '/document/writer/{username}/{bookSlug}/{docSlug}';
38+
const treeShowPath = typeof showPath !== 'undefined' ? showPath : true;
3739
3840
// 默认图标配置
3941
const defaultIcons = {
@@ -146,7 +148,9 @@ function generateDocumentLink(doc) {
146148
147149
<div class="flex items-center space-x-2">
148150
<span class="truncate text-sm"><%= doc.title %></span>
149-
<span class="text-xs <%= isCurrentDocument(doc) ? 'text-gray-200' : 'text-gray-400' %> flex-shrink-0">(<%= doc.slug %>)</span>
151+
<% if (treeShowPath) { %>
152+
<span class="text-xs <%= isCurrentDocument(doc) ? 'text-gray-200' : 'text-gray-400' %> flex-shrink-0">(<%= doc.slug %>)</span>
153+
<% } %>
150154
<% if (treeShowStatusBadge) { %>
151155
<% const statusValue = doc[treeStatusField] || doc.status || ''; %>
152156
<% if (statusValue) { %>

frontend/views/pages/book/chapters.ejs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@
4545
<i class="fas fa-file-alt text-gray-300 text-6xl mb-4"></i>
4646
<h3 class="text-lg font-medium text-gray-900 mb-2">暂无文档</h3>
4747
<p class="text-gray-500 mb-6">这本书还没有任何文档,开始创建第一个文档吧</p>
48-
<a href="/document/writer/<%= book.username %>/<%= book.slug %>"
48+
<a href="/book/writer/<%= book.username %>/<%= book.slug %>"
4949
class="inline-flex items-center px-4 py-2 bg-blue-500 hover:bg-blue-600 text-white rounded-lg transition-colors">
5050
<i class="fas fa-plus mr-2"></i>
5151
创建文档

frontend/views/pages/book/info.ejs

Lines changed: 57 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -20,46 +20,17 @@
2020
</p>
2121
</div>
2222

23-
<form method="POST" action="<%= isEdit ? `/book/${ book.username }/${ book.slug }/edit` : '/book/create' %>" enctype="multipart/form-data" class="p-6 space-y-6">
23+
<form method="POST" action="<%= isEdit ? `/book/${ book.username }/${ book.slug }/edit` : '/book/create' %>" enctype="multipart/form-data" class="<%= isEdit ? 'p-6 pt-0' : 'p-6' %> space-y-6">
2424
<%- include('../../components/alert', { type: 'error', message: error }) %>
2525
<%- include('../../components/alert', { type: 'success', message: success }) %>
2626
<% if (isEdit) { %>
27-
<input type="hidden" name="_method" value="PUT">
27+
<input type="hidden" class="hidden" name="_method" value="PUT">
2828
<% } %>
2929

30-
<!-- 书籍标题 -->
31-
<div>
32-
<label for="title" class="block text-sm font-medium text-gray-700 mb-2">
33-
书籍标题 <span class="text-red-500">*</span>
34-
</label>
35-
<%- include('../../components/input', { type: 'text', name: 'title', id: 'title', value: isEdit ? book.title : '', required: true, placeholder: '请输入书籍标题' }) %>
36-
</div>
37-
38-
<!-- URL路径 -->
39-
<div>
40-
<label for="slug" class="block text-sm font-medium text-gray-700 mb-2">
41-
URL路径 <span class="text-red-500">*</span>
42-
</label>
43-
<div class="flex items-center">
44-
<%- include('../../components/input', { type: 'text', name: 'slug', prefix: `/book/${ isEdit ? book.username : user.username }/`, id: 'slug', value: isEdit ? book.slug : '', required: true, placeholder: '请输入URL路径' }) %>
45-
</div>
46-
<p class="mt-1 text-xs text-gray-500">
47-
只能包含小写字母、数字和连字符,将用于书籍的访问地址
48-
</p>
49-
</div>
50-
51-
<!-- 书籍描述 -->
52-
<div>
53-
<label for="description" class="block text-sm font-medium text-gray-700 mb-2">
54-
书籍描述
55-
</label>
56-
<%- include('../../components/textarea', { name: 'description', id: 'description', value: isEdit ? book.description : '', placeholder: '请输入书籍描述' }) %>
57-
</div>
58-
5930
<!-- 封面图片 -->
6031
<div>
6132
<label for="cover_image" class="block text-sm font-medium text-gray-700 mb-2">
62-
封面图片
33+
封面图片 <span class="text-red-500">*</span>
6334
</label>
6435

6536
<% if (isEdit && book.cover_image) { %>
@@ -90,6 +61,37 @@
9061
</p>
9162
</div>
9263

64+
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
65+
<!-- 书籍标题 -->
66+
<div>
67+
<label for="title" class="block text-sm font-medium text-gray-700 mb-2">
68+
书籍标题 <span class="text-red-500">*</span>
69+
</label>
70+
<%- include('../../components/input', { type: 'text', name: 'title', id: 'title', value: isEdit ? book.title : '', required: true, placeholder: '请输入书籍标题' }) %>
71+
</div>
72+
73+
<!-- URL路径 -->
74+
<div>
75+
<label for="slug" class="block text-sm font-medium text-gray-700 mb-2">
76+
URL路径 <span class="text-red-500">*</span>
77+
</label>
78+
<div class="flex items-center">
79+
<%- include('../../components/input', { type: 'text', name: 'slug', prefix: `/book/${ isEdit ? book.username : user.username }/`, id: 'slug', value: isEdit ? book.slug : '', required: true, placeholder: '请输入URL路径' }) %>
80+
</div>
81+
<p class="mt-1 text-xs text-gray-500">
82+
只能包含小写字母、数字和连字符,将用于书籍的访问地址
83+
</p>
84+
</div>
85+
</div>
86+
87+
<!-- 书籍描述 -->
88+
<div>
89+
<label for="description" class="block text-sm font-medium text-gray-700 mb-2">
90+
书籍描述
91+
</label>
92+
<%- include('../../components/textarea', { name: 'description', id: 'description', value: isEdit ? book.description : '', placeholder: '请输入书籍描述' }) %>
93+
</div>
94+
9395
<!-- 状态设置 -->
9496
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
9597
<!-- 发布状态 -->
@@ -112,11 +114,33 @@
112114
</div>
113115
</div>
114116

117+
<!-- 排序 -->
118+
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
119+
<!-- 排序字段 -->
120+
<div>
121+
<label for="order_col" class="block text-sm font-medium text-gray-700 mb-2">
122+
排序字段
123+
</label>
124+
<%- include('../../components/select', { id: 'order_col', name: 'order_col', value: isEdit ? book.order_col : 'created_at', required: true, options: [ { value: 'created_at', label: '创建时间' }, { value: 'updated_at', label: '更新时间' }, { value: 'view_count', label: '浏览量' } ] }) %>
125+
<p class="mt-1 text-xs text-gray-500">
126+
用于标记书籍的排序字段,只有在书籍目录和阅读时生效。
127+
</p>
128+
</div>
129+
130+
<!-- 排序方式 -->
131+
<div>
132+
<label for="order_dir" class="block text-sm font-medium text-gray-700 mb-2">
133+
排序方式
134+
</label>
135+
<%- include('../../components/select', { id: 'order_dir', name: 'order_dir', value: isEdit ? book.order_dir : 'asc', required: true, options: [ { value: 'asc', label: '升序' }, { value: 'desc', label: '降序' } ] }) %>
136+
</div>
137+
</div>
138+
115139
<!-- 操作按钮 -->
116140
<div class="flex items-center justify-between pt-6 border-t border-gray-200">
117141
<div class="flex space-x-3">
118142
<%- include('../../components/button', { type: 'submit', icon: 'fas fa-save', text: isEdit ? '保存修改' : '创建书籍' }) %>
119-
<%- include('../../components/button', { href: isEdit ? `/books/${ book.slug }` : '/books', icon: 'fas fa-times', text: '取消', variant: 'outline' }) %>
143+
<%- include('../../components/button', { href: isEdit ? `/book/${authUser.username}/${ book.slug }` : '/book/${authUser.username}', icon: 'fas fa-times', text: '取消', variant: 'outline' }) %>
120144
</div>
121145
</div>
122146
</form>

0 commit comments

Comments
 (0)