fix: 优化书籍后台字段展示与提交
Some checks failed
CI / init (pull_request) Has been cancelled
CI / Frontend node 18.16.0 (pull_request) Has been cancelled
CI / Backend go (1.22) (pull_request) Has been cancelled
CI / devops-test (1.22, 18.16.0) (pull_request) Has been cancelled
CI / release-pr (pull_request) Has been cancelled
CI / release-please (pull_request) Has been cancelled
CI / devops-prod (1.22, 18.x) (pull_request) Has been cancelled
CI / docker (pull_request) Has been cancelled

This commit is contained in:
2026-04-27 13:55:21 +08:00
parent 392fbea7eb
commit c31466aeb5
7 changed files with 499 additions and 18 deletions

View File

@@ -38,6 +38,10 @@ flowchart LR
- `path` 默认与 `name` 同步;只有明确需要参数化路径时才额外拼接,不要把查询条件硬塞进路由 path。
- 需要缓存页签时设置 `meta.keepAlive`;需要进入后自动关闭 tab 时设置 `meta.closeTab`
- 页面进入菜单体系后,新增菜单不等于可访问;还必须补角色授权,否则页面可能存在但用户不可见。
- 列表页的 list item 遇到单图片字段时,必须使用图片预览组件展示;禁止把图片 URL、图片路径或单图片附件地址作为普通文本直接显示。
- 新增/编辑功能遇到图片或文件属性时,必须使用上传组件并支持已有值回显;禁止使用 `el-input``textarea` 或普通文本输入组件让用户手填 URL/路径。
- 列表页、详情页、关联表、related list item 遇到字典字段时,必须声明对应 `dict` 配置并使用字典格式化展示 `Label`;禁止把字典 `Value/key` 作为普通文本直接显示。
- 字典字段在新增/编辑/筛选中提交和绑定使用 `Value`,页面展示统一使用字典 `Label`;涉及字典编码和值域时必须先读取后台 `doc-dict` 文档。
## curl 联调案例
@@ -69,3 +73,6 @@ curl --location --request POST "$BASE_URL/menu/addBaseMenu" \
- 远程路由 `component` 写错路径格式,导致 `asyncRouter.js` 找不到页面组件。
- 页面里直接写 axios 请求或直接拼 token绕过 `src/api``src/utils/request.js`
- 改了路由 `name/path`,没有同步检查 `keepAlive`、菜单高亮、`defaultRouter` 和未登录跳转。
- list item 的单图片字段直接显示 URL 文本,没有使用图片预览。
- 新增/编辑表单把图片或文件字段做成文本输入框,要求用户手动填写 URL 或路径。
- 详情页或关联表字段直接显示字典 key例如 `draft``on_shelf``completed`,没有通过字典格式化显示名称。

View File

@@ -3,8 +3,8 @@
"version": "2.9.1",
"private": true,
"scripts": {
"dev": "node openDocument.js && vite --host --mode development",
"serve": "node openDocument.js && vite --host --mode development",
"dev": "vite --host --mode development",
"serve": "vite --host --mode development",
"build": "vite build --mode production",
"limit-build": "npm install increase-memory-limit-fixbug cross-env -g && npm run fix-memory-limit && node ./limit && npm run build",
"preview": "vite preview",

View File

@@ -194,8 +194,14 @@
<div v-for="block in relatedBlocks" :key="block.title" class="book-admin-related">
<div class="book-admin-related__title">{{ block.title }}</div>
<el-table :data="block.rows" size="small" border>
<el-table-column v-for="column in block.columns" :key="column" :label="column" :prop="column" show-overflow-tooltip>
<template #default="scope">{{ formatLooseCell(scope.row[column]) }}</template>
<el-table-column
v-for="column in block.columns"
:key="column.prop"
:label="column.label"
:prop="column.prop"
show-overflow-tooltip
>
<template #default="scope">{{ formatRelatedCell(scope.row, column) }}</template>
</el-table-column>
</el-table>
</div>
@@ -235,7 +241,17 @@
</el-form-item>
</el-form>
<el-table :data="authorRelations" border>
<el-table-column label="作者 ID" prop="authorId" width="120" />
<el-table-column
v-for="column in bookAuthorRelationDisplayColumns"
:key="column.prop"
:label="column.label"
:min-width="column.minWidth"
:prop="column.prop"
:width="column.width"
show-overflow-tooltip
>
<template #default="scope">{{ formatLooseCell(scope.row[column.prop]) }}</template>
</el-table-column>
<el-table-column label="排序" prop="authorSort" width="180">
<template #default="scope">
<el-input-number v-model="scope.row.authorSort" :min="1" :step="1" @change="updateAuthorSort(scope.row)" />
@@ -258,7 +274,9 @@
import { getUrl } from '@/utils/image'
import { filterDict, formatBoolean, formatDate, getBaseUrl } from '@/utils/format'
import { useUserStore } from '@/pinia'
import { bookAuthorRelationDisplayColumns } from '../config/bookAdminConfig'
import { isBookAdminImageColumn, resolveBookAdminColumnType } from '../config/bookAdminDisplay'
import { buildBookAdminSubmitPayload, buildBookAuthorRelationSubmitPayload } from '../config/bookAdminPayload'
import { ElMessage, ElMessageBox } from 'element-plus'
import { computed, reactive, ref } from 'vue'
@@ -371,6 +389,19 @@
return value ?? ''
}
const normalizeRelatedColumn = (column) => {
return typeof column === 'string' ? { label: column, prop: column } : column
}
const formatRelatedCell = (row, column) => {
const value = row?.[column.prop]
if (column.type === 'datetime') return formatDate(value)
if (column.type === 'date') return value ? String(value).slice(0, 10) : ''
if (column.type === 'boolean' || column.type === 'switch') return formatBoolean(value)
if (column.type === 'dict') return filterDict(value, dictOptions[column.dict]) || value || ''
return formatLooseCell(value)
}
const formatCell = (row, column) => {
const value = row?.[column.prop]
const columnType = resolveBookAdminColumnType(config.value, column)
@@ -419,6 +450,11 @@
;[...config.value.searchFields, ...config.value.fields, ...config.value.tableColumns].forEach((field) => {
if (field.dict) dictCodes.add(field.dict)
})
config.value.related?.forEach((relation) => {
relation.columns?.map(normalizeRelatedColumn).forEach((column) => {
if (column.dict) dictCodes.add(column.dict)
})
})
config.value.statusFields?.forEach((status) => {
if (status.dict) dictCodes.add(status.dict)
})
@@ -525,7 +561,8 @@
const enterDialog = () => {
formRef.value?.validate(async (valid) => {
if (!valid) return
const res = dialogType.value === 'update' ? await api.value.update(formData.value) : await api.value.create(formData.value)
const payload = buildBookAdminSubmitPayload(config.value, formData.value, dialogType.value)
const res = dialogType.value === 'update' ? await api.value.update(payload) : await api.value.create(payload)
if (res.code === 0) {
ElMessage({ type: 'success', message: '创建/更改成功' })
closeDialog()
@@ -576,7 +613,8 @@
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
const res = await api.value.update({ ...row, [status.prop]: value })
const payload = buildBookAdminSubmitPayload(config.value, { ...row, [status.prop]: value }, 'update')
const res = await api.value.update(payload)
if (res.code === 0) {
ElMessage({ type: 'success', message: '状态已调整' })
getTableData()
@@ -593,7 +631,7 @@
if (!listApi) continue
const res = await listApi({ page: 1, pageSize: 10, [relation.filterProp]: row.id })
if (res.code === 0) {
blocks.push({ title: relation.title, columns: relation.columns, rows: res.data.list || [] })
blocks.push({ title: relation.title, columns: relation.columns.map(normalizeRelatedColumn), rows: res.data.list || [] })
}
}
relatedBlocks.value = blocks
@@ -659,7 +697,7 @@
}
const updateAuthorSort = async (row) => {
const res = await bookApi.updateBookAuthorRelation(row)
const res = await bookApi.updateBookAuthorRelation(buildBookAuthorRelationSubmitPayload(row, 'update'))
if (res.code === 0) ElMessage({ type: 'success', message: '排序已更新' })
}

View File

@@ -23,6 +23,51 @@ const imageField = (label, prop, extra = {}) => ({ label, prop, type: 'image', .
const fileField = (label, prop, extra = {}) => ({ label, prop, type: 'file', ...extra })
const dictField = (label, prop, dict, extra = {}) => ({ label, prop, type: 'dict', dict, ...extra })
const relationField = (label, prop, relation, extra = {}) => ({ label, prop, type: 'relation', relation, ...extra })
const relatedColumn = (label, prop, extra = {}) => ({ label, prop, ...extra })
export const bookAuthorRelationDisplayColumns = [
{ label: '作者名称', prop: 'authorName', minWidth: 160 },
{ label: '作者 ID', prop: 'authorId', width: 120 }
]
const bookSubmitFields = [
'id',
'title',
'subtitle',
'bookType',
'eraTag',
'coverUrl',
'publisher',
'publishedAt',
'intro',
'hotScore',
'rating',
'commentCount',
'wordCount',
'completionStatus',
'publishStatus',
'seriesId',
'seriesSort',
'rawTxtUrl'
]
const chapterSubmitFields = [
'id',
'bookId',
'title',
'chapterNo',
'isReadable',
'contentFileUrl',
'totalLines',
'isEnabled'
]
const authorSubmitFields = ['id', 'name', 'isEnabled', 'intro', 'coverUrl']
const seriesSubmitFields = ['id', 'name', 'coverUrl', 'intro', 'isEnabled']
const commentSubmitFields = ['id', 'memberUserId', 'bookId', 'chapterId', 'lineIndex', 'content', 'likeCount', 'commentStatus']
const readRecordSubmitFields = ['id', 'memberUserId', 'bookId', 'bookTitleSnapshot', 'readProgress', 'chapterId', 'lineIndex', 'lastReadAt']
const favoriteRecordSubmitFields = ['id', 'memberUserId', 'bookId', 'favoritedAt']
const commentLikeRecordSubmitFields = ['id', 'commentId', 'memberUserId', 'likedAt']
export const bookAdminPageConfigs = [
{
@@ -37,6 +82,7 @@ export const bookAdminPageConfigs = [
find: 'findBook',
list: 'getBookList'
},
submitFields: bookSubmitFields,
searchFields: [
textField('书名', 'title'),
dictField('书籍类型', 'bookType', 'book_type'),
@@ -79,9 +125,9 @@ export const bookAdminPageConfigs = [
{ prop: 'completionStatus', label: '完结状态', dict: 'book_completion_status' }
],
related: [
{ title: '关联作者', api: 'getBookAuthorRelationList', filterProp: 'bookId', columns: ['authorId', 'authorSort'] },
{ title: '关联作者', api: 'getBookAuthorRelationList', filterProp: 'bookId', columns: ['authorName', 'authorId', 'authorSort'] },
{ title: '关联章节', api: 'getBookChapterList', filterProp: 'bookId', columns: ['id', 'title', 'chapterNo', 'isReadable', 'isEnabled'] },
{ title: '关联评论', api: 'getBookCommentList', filterProp: 'bookId', columns: ['id', 'memberUserId', 'chapterId', 'lineIndex', 'commentStatus'] }
{ title: '关联评论', api: 'getBookCommentList', filterProp: 'bookId', columns: ['id', 'memberUserId', 'chapterTitle', 'chapterId', 'lineIndex', 'commentStatus'] }
],
authorBinding: true
},
@@ -97,6 +143,7 @@ export const bookAdminPageConfigs = [
find: 'findBookChapter',
list: 'getBookChapterList'
},
submitFields: chapterSubmitFields,
searchFields: [
relationField('所属书籍', 'bookId', { listApi: 'getBookList', labelProp: 'title', valueProp: 'id', searchProp: 'title' }),
switchField('开放阅读', 'isReadable'),
@@ -105,6 +152,7 @@ export const bookAdminPageConfigs = [
tableColumns: [
idColumn,
{ label: '章节标题', prop: 'title', minWidth: 180 },
{ label: '书名', prop: 'bookTitle', minWidth: 180 },
{ label: '书籍 ID', prop: 'bookId', width: 100 },
{ label: '章节序号', prop: 'chapterNo', width: 110 },
{ label: '开放阅读', prop: 'isReadable', type: 'boolean', width: 100 },
@@ -138,28 +186,29 @@ export const bookAdminPageConfigs = [
find: 'findBookAuthor',
list: 'getBookAuthorList'
},
submitFields: authorSubmitFields,
searchFields: [
textField('作者名称', 'name'),
dictField('作者状态', 'authorStatus', 'book_author_status')
switchField('启用状态', 'isEnabled')
],
tableColumns: [
idColumn,
{ label: '作者名称', prop: 'name', minWidth: 160 },
{ label: '作者状态', prop: 'authorStatus', type: 'dict', dict: 'book_author_status', width: 120 },
{ label: '启用状态', prop: 'isEnabled', type: 'boolean', width: 100 },
{ label: '头像/封面 URL', prop: 'coverUrl', minWidth: 180 },
dateColumn
],
fields: [
textField('作者名称', 'name', { required: true }),
dictField('作者状态', 'authorStatus', 'book_author_status', { required: true, defaultValue: 'enabled' }),
switchField('作者是否启用', 'isEnabled', { defaultValue: true }),
textareaField('作者简介', 'intro'),
imageField('作者头像或封面', 'coverUrl')
],
statusFields: [
{ prop: 'authorStatus', label: '作者状态', dict: 'book_author_status' }
{ prop: 'isEnabled', label: '启用状态', type: 'boolean' }
],
related: [
{ title: '关联书籍', api: 'getBookAuthorRelationList', filterProp: 'authorId', columns: ['bookId', 'authorSort'] }
{ title: '关联书籍', api: 'getBookAuthorRelationList', filterProp: 'authorId', columns: ['bookTitle', 'bookId', 'authorSort'] }
]
},
{
@@ -174,6 +223,7 @@ export const bookAdminPageConfigs = [
find: 'findBookSeries',
list: 'getBookSeriesList'
},
submitFields: seriesSubmitFields,
searchFields: [
textField('系列名称', 'name'),
switchField('启用状态', 'isEnabled')
@@ -195,7 +245,18 @@ export const bookAdminPageConfigs = [
{ prop: 'isEnabled', label: '启用状态', type: 'boolean' }
],
related: [
{ title: '系列下书籍', api: 'getBookList', filterProp: 'seriesId', columns: ['id', 'title', 'publishStatus', 'completionStatus', 'seriesSort'] }
{
title: '系列下书籍',
api: 'getBookList',
filterProp: 'seriesId',
columns: [
relatedColumn('ID', 'id'),
relatedColumn('书名', 'title'),
relatedColumn('上下架状态', 'publishStatus', { type: 'dict', dict: 'book_publish_status' }),
relatedColumn('完结状态', 'completionStatus', { type: 'dict', dict: 'book_completion_status' }),
relatedColumn('同系列排序', 'seriesSort')
]
}
]
},
{
@@ -210,6 +271,7 @@ export const bookAdminPageConfigs = [
find: 'findBookComment',
list: 'getBookCommentList'
},
submitFields: commentSubmitFields,
searchFields: [
numberField('会员用户 ID', 'memberUserId'),
numberField('书籍 ID', 'bookId'),
@@ -219,7 +281,9 @@ export const bookAdminPageConfigs = [
tableColumns: [
idColumn,
{ label: '会员用户 ID', prop: 'memberUserId', width: 120 },
{ label: '书名', prop: 'bookTitle', minWidth: 180 },
{ label: '书籍 ID', prop: 'bookId', width: 100 },
{ label: '章节标题', prop: 'chapterTitle', minWidth: 180 },
{ label: '章节 ID', prop: 'chapterId', width: 100 },
{ label: '行序号', prop: 'lineIndex', width: 90 },
{ label: '评论内容', prop: 'content', minWidth: 220 },
@@ -253,6 +317,7 @@ export const bookAdminPageConfigs = [
find: 'findBookReadRecord',
list: 'getBookReadRecordList'
},
submitFields: readRecordSubmitFields,
searchFields: [numberField('会员用户 ID', 'memberUserId'), numberField('书籍 ID', 'bookId')],
tableColumns: [
idColumn,
@@ -260,6 +325,7 @@ export const bookAdminPageConfigs = [
{ label: '书籍 ID', prop: 'bookId', width: 100 },
{ label: '书名快照', prop: 'bookTitleSnapshot', minWidth: 180 },
{ label: '阅读进度', prop: 'readProgress', width: 100 },
{ label: '章节标题', prop: 'chapterTitle', minWidth: 180 },
{ label: '续读章节 ID', prop: 'chapterId', width: 120 },
{ label: '续读行序号', prop: 'lineIndex', width: 120 },
{ label: '最后阅读时间', prop: 'lastReadAt', type: 'datetime', width: 180 }
@@ -287,10 +353,12 @@ export const bookAdminPageConfigs = [
find: 'findBookFavoriteRecord',
list: 'getBookFavoriteRecordList'
},
submitFields: favoriteRecordSubmitFields,
searchFields: [numberField('会员用户 ID', 'memberUserId'), numberField('书籍 ID', 'bookId')],
tableColumns: [
idColumn,
{ label: '会员用户 ID', prop: 'memberUserId', width: 120 },
{ label: '书名', prop: 'bookTitle', minWidth: 180 },
{ label: '书籍 ID', prop: 'bookId', width: 100 },
{ label: '收藏时间', prop: 'favoritedAt', type: 'datetime', width: 180 },
dateColumn
@@ -314,9 +382,12 @@ export const bookAdminPageConfigs = [
find: 'findBookCommentLikeRecord',
list: 'getBookCommentLikeRecordList'
},
submitFields: commentLikeRecordSubmitFields,
searchFields: [numberField('评论 ID', 'commentId'), numberField('会员用户 ID', 'memberUserId')],
tableColumns: [
idColumn,
{ label: '书名', prop: 'bookTitle', minWidth: 180 },
{ label: '评论内容', prop: 'commentContent', minWidth: 220 },
{ label: '评论 ID', prop: 'commentId', width: 100 },
{ label: '会员用户 ID', prop: 'memberUserId', width: 120 },
{ label: '点赞时间', prop: 'likedAt', type: 'datetime', width: 180 },

View File

@@ -1,13 +1,44 @@
import assert from 'node:assert/strict'
import { bookAdminPageConfigs } from './bookAdminConfig.js'
import { bookAdminPageConfigs, bookAuthorRelationDisplayColumns } from './bookAdminConfig.js'
import { isBookAdminImageColumn, resolveBookAdminColumnType } from './bookAdminDisplay.js'
const getConfig = (key) => bookAdminPageConfigs.find((item) => item.key === key)
const getColumn = (config, prop) => config.tableColumns.find((item) => item.prop === prop)
const getRelated = (config, title) => config.related.find((item) => item.title === title)
const getRelatedColumn = (related, prop) => related.columns.find((item) => (typeof item === 'string' ? item : item.prop) === prop)
const authorConfig = getConfig('author')
const bookConfig = getConfig('book')
const chapterConfig = getConfig('chapter')
const commentConfig = getConfig('comment')
const commentLikeRecordConfig = getConfig('commentLikeRecord')
const favoriteRecordConfig = getConfig('favoriteRecord')
const readRecordConfig = getConfig('readRecord')
const seriesConfig = getConfig('series')
assert.equal(getColumn(authorConfig, 'isEnabled').type, 'boolean')
assert.equal(authorConfig.searchFields.find((item) => item.prop === 'isEnabled').type, 'switch')
assert.equal(authorConfig.fields.find((item) => item.prop === 'isEnabled').type, 'switch')
assert.equal(authorConfig.fields.find((item) => item.prop === 'isEnabled').defaultValue, true)
assert.equal(authorConfig.statusFields.find((item) => item.prop === 'isEnabled').type, 'boolean')
assert.equal(authorConfig.searchFields.some((item) => item.prop === 'authorStatus'), false)
assert.equal(authorConfig.fields.some((item) => item.prop === 'authorStatus'), false)
assert.deepEqual(bookAuthorRelationDisplayColumns.map((item) => item.prop), ['authorName', 'authorId'])
assert.deepEqual(getRelated(bookConfig, '关联作者').columns, ['authorName', 'authorId', 'authorSort'])
assert.deepEqual(getRelated(authorConfig, '关联书籍').columns, ['bookTitle', 'bookId', 'authorSort'])
assert.deepEqual(getRelated(bookConfig, '关联评论').columns, ['id', 'memberUserId', 'chapterTitle', 'chapterId', 'lineIndex', 'commentStatus'])
assert.equal(getColumn(chapterConfig, 'bookTitle')?.label, '书名')
assert.equal(getColumn(commentConfig, 'bookTitle')?.label, '书名')
assert.equal(getColumn(commentConfig, 'chapterTitle')?.label, '章节标题')
assert.equal(getColumn(commentLikeRecordConfig, 'bookTitle')?.label, '书名')
assert.equal(getColumn(commentLikeRecordConfig, 'commentContent')?.label, '评论内容')
assert.equal(getColumn(favoriteRecordConfig, 'bookTitle')?.label, '书名')
assert.equal(getColumn(readRecordConfig, 'chapterTitle')?.label, '章节标题')
assert.equal(getRelatedColumn(getRelated(seriesConfig, '系列下书籍'), 'publishStatus').type, 'dict')
assert.equal(getRelatedColumn(getRelated(seriesConfig, '系列下书籍'), 'publishStatus').dict, 'book_publish_status')
assert.equal(getRelatedColumn(getRelated(seriesConfig, '系列下书籍'), 'completionStatus').type, 'dict')
assert.equal(getRelatedColumn(getRelated(seriesConfig, '系列下书籍'), 'completionStatus').dict, 'book_completion_status')
assert.equal(resolveBookAdminColumnType(authorConfig, getColumn(authorConfig, 'coverUrl')), 'image')
assert.equal(resolveBookAdminColumnType(seriesConfig, getColumn(seriesConfig, 'coverUrl')), 'image')

View File

@@ -0,0 +1,31 @@
export const buildBookAdminSubmitPayload = (config, formData, dialogType) => {
if (!config?.submitFields?.length) return formData
const source = formData || {}
const fields = dialogType === 'update'
? config.submitFields
: config.submitFields.filter((prop) => prop !== 'id')
return fields.reduce((payload, prop) => {
if (Object.prototype.hasOwnProperty.call(source, prop)) {
payload[prop] = source[prop]
}
return payload
}, {})
}
const bookAuthorRelationSubmitFields = ['id', 'bookId', 'authorId', 'authorSort']
export const buildBookAuthorRelationSubmitPayload = (formData, dialogType) => {
const source = formData || {}
const fields = dialogType === 'update'
? bookAuthorRelationSubmitFields
: bookAuthorRelationSubmitFields.filter((prop) => prop !== 'id')
return fields.reduce((payload, prop) => {
if (Object.prototype.hasOwnProperty.call(source, prop)) {
payload[prop] = source[prop]
}
return payload
}, {})
}

View File

@@ -0,0 +1,303 @@
import assert from 'node:assert/strict'
import { bookAdminPageConfigs } from './bookAdminConfig.js'
import { buildBookAdminSubmitPayload, buildBookAuthorRelationSubmitPayload } from './bookAdminPayload.js'
const getConfig = (key) => bookAdminPageConfigs.find((item) => item.key === key)
const authorConfig = getConfig('author')
const bookConfig = getConfig('book')
const chapterConfig = getConfig('chapter')
const commentConfig = getConfig('comment')
const commentLikeRecordConfig = getConfig('commentLikeRecord')
const favoriteRecordConfig = getConfig('favoriteRecord')
const readRecordConfig = getConfig('readRecord')
const seriesConfig = getConfig('series')
assert.equal(bookAdminPageConfigs.every((config) => Array.isArray(config.submitFields) && config.submitFields.length > 0), true)
assert.deepEqual(
buildBookAdminSubmitPayload(
authorConfig,
{
id: 12,
name: '鲁迅',
isEnabled: false,
intro: '作者简介',
coverUrl: '/uploads/author.png',
authorName: '鲁迅',
createdAt: '2026-04-01 10:00:00',
updatedAt: '2026-04-02 10:00:00'
},
'update'
),
{
id: 12,
name: '鲁迅',
isEnabled: false,
intro: '作者简介',
coverUrl: '/uploads/author.png'
}
)
assert.deepEqual(
buildBookAdminSubmitPayload(
authorConfig,
{
id: 12,
name: '鲁迅',
isEnabled: true,
intro: '',
coverUrl: '',
createdAt: '2026-04-01 10:00:00'
},
'create'
),
{
name: '鲁迅',
isEnabled: true,
intro: '',
coverUrl: ''
}
)
assert.deepEqual(
buildBookAdminSubmitPayload(
authorConfig,
{
id: 13,
name: '禁用作者',
isEnabled: false,
intro: '',
coverUrl: '',
createdAt: '2026-04-01 10:00:00'
},
'create'
),
{
name: '禁用作者',
isEnabled: false,
intro: '',
coverUrl: ''
}
)
assert.deepEqual(
buildBookAdminSubmitPayload(
bookConfig,
{
id: 1,
title: '测试书籍',
subtitle: '副标题',
bookType: 'novel',
eraTag: 'unknown',
coverUrl: '/uploads/book.png',
publisher: '测试出版社',
publishedAt: '2026-04-01',
intro: '简介',
hotScore: 0,
rating: 8.5,
commentCount: 0,
wordCount: 1000,
completionStatus: 'serializing',
publishStatus: 'draft',
seriesId: 3,
seriesSort: 0,
rawTxtUrl: '/uploads/book.txt',
createdAt: '2026-04-01 10:00:00',
updatedAt: '2026-04-02 10:00:00'
},
'update'
),
{
id: 1,
title: '测试书籍',
subtitle: '副标题',
bookType: 'novel',
eraTag: 'unknown',
coverUrl: '/uploads/book.png',
publisher: '测试出版社',
publishedAt: '2026-04-01',
intro: '简介',
hotScore: 0,
rating: 8.5,
commentCount: 0,
wordCount: 1000,
completionStatus: 'serializing',
publishStatus: 'draft',
seriesId: 3,
seriesSort: 0,
rawTxtUrl: '/uploads/book.txt'
}
)
assert.deepEqual(
buildBookAdminSubmitPayload(chapterConfig, {
id: 9,
bookId: 1,
title: '第一章',
chapterNo: 1,
isReadable: false,
contentFileUrl: '/uploads/chapter.txt',
totalLines: 0,
isEnabled: false,
createdAt: '2026-04-01 10:00:00'
}, 'update'),
{
id: 9,
bookId: 1,
title: '第一章',
chapterNo: 1,
isReadable: false,
contentFileUrl: '/uploads/chapter.txt',
totalLines: 0,
isEnabled: false
}
)
assert.deepEqual(
buildBookAdminSubmitPayload(chapterConfig, {
id: 10,
bookId: 1,
title: '第二章',
chapterNo: 2,
isReadable: false,
contentFileUrl: '/uploads/chapter-2.txt',
totalLines: 0,
isEnabled: false,
createdAt: '2026-04-01 10:00:00'
}, 'create'),
{
bookId: 1,
title: '第二章',
chapterNo: 2,
isReadable: false,
contentFileUrl: '/uploads/chapter-2.txt',
totalLines: 0,
isEnabled: false
}
)
assert.deepEqual(
buildBookAdminSubmitPayload(seriesConfig, {
id: 6,
name: '系列',
coverUrl: '',
intro: '',
isEnabled: false,
updatedAt: '2026-04-02 10:00:00'
}, 'create'),
{
name: '系列',
coverUrl: '',
intro: '',
isEnabled: false
}
)
assert.deepEqual(
buildBookAdminSubmitPayload(commentConfig, {
id: 31,
memberUserId: 8,
bookId: 1,
bookTitle: '边城',
chapterId: 4,
chapterTitle: '第一章 茶峒',
lineIndex: 6,
content: '这段很有画面感',
likeCount: 2,
commentStatus: 'normal',
createdAt: '2026-04-01 10:00:00'
}, 'update'),
{
id: 31,
memberUserId: 8,
bookId: 1,
chapterId: 4,
lineIndex: 6,
content: '这段很有画面感',
likeCount: 2,
commentStatus: 'normal'
}
)
assert.deepEqual(
buildBookAdminSubmitPayload(readRecordConfig, {
id: 41,
memberUserId: 8,
bookId: 1,
bookTitleSnapshot: '边城',
readProgress: 33.5,
chapterId: 4,
chapterTitle: '第一章 茶峒',
lineIndex: 10,
lastReadAt: '2026-04-02 10:00:00',
updatedAt: '2026-04-02 10:00:00'
}, 'update'),
{
id: 41,
memberUserId: 8,
bookId: 1,
bookTitleSnapshot: '边城',
readProgress: 33.5,
chapterId: 4,
lineIndex: 10,
lastReadAt: '2026-04-02 10:00:00'
}
)
assert.deepEqual(
buildBookAdminSubmitPayload(favoriteRecordConfig, {
id: 51,
memberUserId: 8,
bookId: 1,
bookTitle: '边城',
favoritedAt: '2026-04-02 10:00:00',
createdAt: '2026-04-01 10:00:00'
}, 'update'),
{
id: 51,
memberUserId: 8,
bookId: 1,
favoritedAt: '2026-04-02 10:00:00'
}
)
assert.deepEqual(
buildBookAdminSubmitPayload(commentLikeRecordConfig, {
id: 61,
commentId: 31,
memberUserId: 8,
bookTitle: '边城',
commentContent: '这段很有画面感',
likedAt: '2026-04-02 10:00:00',
updatedAt: '2026-04-02 10:00:00'
}, 'update'),
{
id: 61,
commentId: 31,
memberUserId: 8,
likedAt: '2026-04-02 10:00:00'
}
)
assert.deepEqual(
buildBookAuthorRelationSubmitPayload(
{
id: 21,
bookId: 3,
authorId: 12,
authorSort: 2,
bookTitle: '呐喊',
authorName: '鲁迅',
createdAt: '2026-04-01 10:00:00',
updatedAt: '2026-04-02 10:00:00'
},
'update'
),
{
id: 21,
bookId: 3,
authorId: 12,
authorSort: 2
}
)