Compare commits
9 Commits
026c72c4de
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| df75f13816 | |||
| 12d7b7e176 | |||
| ce603c6598 | |||
| a36d195d4d | |||
| 77cbf36524 | |||
| ccd0767194 | |||
| 46b1aa8853 | |||
| cf5f7470c6 | |||
| 9e9f65da76 |
8
.idea/GitCommitMessageStorage.xml
generated
Normal file
8
.idea/GitCommitMessageStorage.xml
generated
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="GitCommitMessageStorage">
|
||||||
|
<option name="messageStorage">
|
||||||
|
<MessageStorage />
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
@@ -2,12 +2,12 @@ import {createRoot} from 'react-dom/client'
|
|||||||
import {createHashRouter, Navigate, type RouteObject, RouterProvider} from 'react-router'
|
import {createHashRouter, Navigate, type RouteObject, RouterProvider} from 'react-router'
|
||||||
import './index.scss'
|
import './index.scss'
|
||||||
import './components/amis/Registry.ts'
|
import './components/amis/Registry.ts'
|
||||||
import Overview from './pages/Overview.tsx'
|
|
||||||
import Root from './pages/Root.tsx'
|
import Root from './pages/Root.tsx'
|
||||||
import Test from './pages/Test.tsx'
|
import Test from './pages/Test.tsx'
|
||||||
import Bookshelf from './pages/book/Bookshelf.tsx'
|
import Bookshelf from './pages/book/Bookshelf.tsx'
|
||||||
import Book from './pages/book/Book.tsx'
|
import Book from './pages/book/Book.tsx'
|
||||||
import Chapter from './pages/book/Chapter.tsx'
|
import Chapter from './pages/book/Chapter.tsx'
|
||||||
|
import Creator from './pages/Creator.tsx'
|
||||||
|
|
||||||
const routes: RouteObject[] = [
|
const routes: RouteObject[] = [
|
||||||
{
|
{
|
||||||
@@ -16,11 +16,7 @@ const routes: RouteObject[] = [
|
|||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
index: true,
|
index: true,
|
||||||
element: <Navigate to="/overview" replace/>,
|
element: <Navigate to="/bookshelf" replace/>,
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'overview',
|
|
||||||
Component: Overview,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'bookshelf',
|
path: 'bookshelf',
|
||||||
@@ -39,6 +35,10 @@ const routes: RouteObject[] = [
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'creator',
|
||||||
|
Component: Creator,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'test',
|
path: 'test',
|
||||||
Component: Test,
|
Component: Test,
|
||||||
|
|||||||
204
client/src/pages/Creator.tsx
Normal file
204
client/src/pages/Creator.tsx
Normal file
@@ -0,0 +1,204 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import {amisRender, commonInfo, horizontalFormOptions} from '../util/amis.tsx'
|
||||||
|
import {type Schema, uuid} from 'amis'
|
||||||
|
|
||||||
|
const aiToolbar = (component: Schema): Schema[] => {
|
||||||
|
const id = uuid()
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
...component,
|
||||||
|
componentId: id,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
className: 'text-right',
|
||||||
|
type: 'button-toolbar',
|
||||||
|
buttons: [
|
||||||
|
{
|
||||||
|
type: 'action',
|
||||||
|
label: 'AI 生成',
|
||||||
|
icon: 'fa fa-brain',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'action',
|
||||||
|
label: 'AI 润色',
|
||||||
|
icon: 'fa fa-edit',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
function Creator() {
|
||||||
|
return (
|
||||||
|
<div className="creator">
|
||||||
|
{amisRender(
|
||||||
|
{
|
||||||
|
type: 'page',
|
||||||
|
title: 'AI创作',
|
||||||
|
body: [
|
||||||
|
{
|
||||||
|
debug: true,
|
||||||
|
type: 'form',
|
||||||
|
...horizontalFormOptions(),
|
||||||
|
wrapWithPanel: false,
|
||||||
|
canAccessSuperData: false,
|
||||||
|
body: [
|
||||||
|
...aiToolbar({
|
||||||
|
type: 'textarea',
|
||||||
|
name: 'outline',
|
||||||
|
label: '故事概述',
|
||||||
|
clearable: true,
|
||||||
|
required: true,
|
||||||
|
trimContents: true,
|
||||||
|
showCounter: true,
|
||||||
|
}),
|
||||||
|
...aiToolbar({
|
||||||
|
type: 'textarea',
|
||||||
|
name: 'world',
|
||||||
|
label: '世界观',
|
||||||
|
clearable: true,
|
||||||
|
required: true,
|
||||||
|
trimContents: true,
|
||||||
|
showCounter: true,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
type: 'input-tag',
|
||||||
|
name: 'tags',
|
||||||
|
label: '标签',
|
||||||
|
placeholder: '',
|
||||||
|
clearable: true,
|
||||||
|
source: `${commonInfo.baseUrl}/book/tags`,
|
||||||
|
max: 10,
|
||||||
|
joinValues: false,
|
||||||
|
extractValue: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'combo',
|
||||||
|
name: 'characters',
|
||||||
|
label: '故事人物',
|
||||||
|
multiLine: true,
|
||||||
|
addable: true,
|
||||||
|
removable: true,
|
||||||
|
multiple: true,
|
||||||
|
subFormMode: 'horizontal',
|
||||||
|
subFormHorizontal: {
|
||||||
|
leftFixed: 'sm',
|
||||||
|
},
|
||||||
|
items: [
|
||||||
|
...aiToolbar({
|
||||||
|
type: 'input-text',
|
||||||
|
name: 'name',
|
||||||
|
label: '名称',
|
||||||
|
clearable: true,
|
||||||
|
required: true,
|
||||||
|
trimContents: true,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
type: 'select',
|
||||||
|
name: 'sex',
|
||||||
|
label: '性别',
|
||||||
|
required: true,
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
label: '男',
|
||||||
|
value: 'male',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '女',
|
||||||
|
value: 'female',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'input-number',
|
||||||
|
name: 'age',
|
||||||
|
label: '年龄',
|
||||||
|
min: 10,
|
||||||
|
step: 1,
|
||||||
|
precision: 0,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
...aiToolbar({
|
||||||
|
type: 'textarea',
|
||||||
|
name: 'appearance',
|
||||||
|
label: '外形',
|
||||||
|
clearable: true,
|
||||||
|
required: true,
|
||||||
|
trimContents: true,
|
||||||
|
showCounter: true,
|
||||||
|
}),
|
||||||
|
...aiToolbar({
|
||||||
|
componentId: 'd8eecb59-153d-4f5e-97b7-a52f4dc2dc58',
|
||||||
|
type: 'textarea',
|
||||||
|
name: 'disposition',
|
||||||
|
label: '性格',
|
||||||
|
clearable: true,
|
||||||
|
required: true,
|
||||||
|
trimContents: true,
|
||||||
|
showCounter: true,
|
||||||
|
}),
|
||||||
|
...aiToolbar({
|
||||||
|
type: 'textarea',
|
||||||
|
name: 'clothes',
|
||||||
|
label: '衣着',
|
||||||
|
clearable: true,
|
||||||
|
trimContents: true,
|
||||||
|
showCounter: true,
|
||||||
|
}),
|
||||||
|
...aiToolbar({
|
||||||
|
type: 'textarea',
|
||||||
|
name: 'experience',
|
||||||
|
label: '经历',
|
||||||
|
clearable: true,
|
||||||
|
trimContents: true,
|
||||||
|
showCounter: true,
|
||||||
|
}),
|
||||||
|
...aiToolbar({
|
||||||
|
type: 'textarea',
|
||||||
|
name: 'family',
|
||||||
|
label: '家庭',
|
||||||
|
clearable: true,
|
||||||
|
trimContents: true,
|
||||||
|
showCounter: true,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
type: 'input-kvs',
|
||||||
|
name: 'extra2',
|
||||||
|
label: '更多信息',
|
||||||
|
draggable: false,
|
||||||
|
keyItem: {
|
||||||
|
type: 'input-text',
|
||||||
|
label: '属性名称',
|
||||||
|
clearable: true,
|
||||||
|
trimContents: true,
|
||||||
|
mode: 'horizontal',
|
||||||
|
horizontal: {
|
||||||
|
leftFixed: 'sm',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
valueItems: aiToolbar({
|
||||||
|
type: 'textarea',
|
||||||
|
name: 'value',
|
||||||
|
label: '属性内容',
|
||||||
|
clearable: true,
|
||||||
|
required: true,
|
||||||
|
trimContents: true,
|
||||||
|
showCounter: true,
|
||||||
|
mode: 'horizontal',
|
||||||
|
horizontal: {
|
||||||
|
leftFixed: 'sm',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default React.memo(Creator)
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
|
|
||||||
function Overview() {
|
|
||||||
return (
|
|
||||||
<div className="overview"></div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default React.memo(Overview)
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import {BookOutlined, DeploymentUnitOutlined, InfoCircleOutlined} from '@ant-design/icons'
|
import {BookOutlined, DeploymentUnitOutlined, EditOutlined, InfoCircleOutlined} from '@ant-design/icons'
|
||||||
import {type AppItemProps, ProLayout} from '@ant-design/pro-components'
|
import {type AppItemProps, ProLayout} from '@ant-design/pro-components'
|
||||||
import {ConfigProvider} from 'antd'
|
import {ConfigProvider} from 'antd'
|
||||||
import React, {useMemo} from 'react'
|
import React, {useMemo} from 'react'
|
||||||
@@ -25,16 +25,16 @@ const menus = {
|
|||||||
name: '概览',
|
name: '概览',
|
||||||
icon: <InfoCircleOutlined/>,
|
icon: <InfoCircleOutlined/>,
|
||||||
routes: [
|
routes: [
|
||||||
{
|
|
||||||
path: '/overview',
|
|
||||||
name: '概览',
|
|
||||||
icon: <InfoCircleOutlined/>,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: '/bookshelf',
|
path: '/bookshelf',
|
||||||
name: '书架',
|
name: '书架',
|
||||||
icon: <BookOutlined/>,
|
icon: <BookOutlined/>,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/creator',
|
||||||
|
name: '创作',
|
||||||
|
icon: <EditOutlined/>,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/test',
|
path: '/test',
|
||||||
name: '测试',
|
name: '测试',
|
||||||
|
|||||||
@@ -9,6 +9,48 @@ import {
|
|||||||
time,
|
time,
|
||||||
} from '../../util/amis.tsx'
|
} from '../../util/amis.tsx'
|
||||||
|
|
||||||
|
const detailDialog = (bookId: string | undefined) => {
|
||||||
|
return {
|
||||||
|
title: '添加书架',
|
||||||
|
size: 'md',
|
||||||
|
body: {
|
||||||
|
debug: commonInfo.debug,
|
||||||
|
type: 'form',
|
||||||
|
api: `${commonInfo.baseUrl}/chapter/save`,
|
||||||
|
initApi: `${commonInfo.baseUrl}/chapter/detail/\${id}`,
|
||||||
|
initFetchOn: '${id}',
|
||||||
|
...horizontalFormOptions(),
|
||||||
|
canAccessSuperData: false,
|
||||||
|
body: [
|
||||||
|
{
|
||||||
|
type: 'hidden',
|
||||||
|
name: 'bookId',
|
||||||
|
value: bookId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'input-number',
|
||||||
|
name: 'sequence',
|
||||||
|
label: '序号',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'input-text',
|
||||||
|
name: 'name',
|
||||||
|
label: '名称',
|
||||||
|
required: true,
|
||||||
|
clearable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'textarea',
|
||||||
|
name: 'description',
|
||||||
|
label: '描述',
|
||||||
|
clearable: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function Book() {
|
function Book() {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const {id} = useParams()
|
const {id} = useParams()
|
||||||
@@ -49,11 +91,11 @@ function Book() {
|
|||||||
sort: [
|
sort: [
|
||||||
{
|
{
|
||||||
column: 'sequence',
|
column: 'sequence',
|
||||||
direction: 'ASC',
|
direction: 'DESC',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
column: 'modifiedTime',
|
column: 'modifiedTime',
|
||||||
direction: 'DESC',
|
direction: 'ASC',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -70,7 +112,9 @@ function Book() {
|
|||||||
actionType: 'ajax',
|
actionType: 'ajax',
|
||||||
tooltip: '序号重排',
|
tooltip: '序号重排',
|
||||||
tooltipPlacement: 'top',
|
tooltipPlacement: 'top',
|
||||||
api: `get:${commonInfo.baseUrl}/chapter/generate_sequence`
|
api: `get:${commonInfo.baseUrl}/chapter/generate_sequence`,
|
||||||
|
confirmText: '确认重排序号?',
|
||||||
|
confirmTitle: '序号重拍',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'action',
|
type: 'action',
|
||||||
@@ -142,7 +186,7 @@ function Book() {
|
|||||||
actionType: 'dialog',
|
actionType: 'dialog',
|
||||||
dialog: detailDialog(),
|
dialog: detailDialog(),
|
||||||
},*/
|
},*/
|
||||||
]
|
],
|
||||||
),
|
),
|
||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
@@ -159,7 +203,6 @@ function Book() {
|
|||||||
{
|
{
|
||||||
name: 'description',
|
name: 'description',
|
||||||
label: '描述',
|
label: '描述',
|
||||||
width: 250,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '创建时间',
|
label: '创建时间',
|
||||||
@@ -179,6 +222,7 @@ function Book() {
|
|||||||
type: 'action',
|
type: 'action',
|
||||||
label: '详情',
|
label: '详情',
|
||||||
level: 'link',
|
level: 'link',
|
||||||
|
size: 'sm',
|
||||||
onEvent: {
|
onEvent: {
|
||||||
click: {
|
click: {
|
||||||
actions: [
|
actions: [
|
||||||
@@ -193,19 +237,20 @@ function Book() {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
/*{
|
{
|
||||||
type: 'action',
|
type: 'action',
|
||||||
label: '修改',
|
label: '修改',
|
||||||
level: 'link',
|
level: 'link',
|
||||||
size: 'sm',
|
size: 'sm',
|
||||||
actionType: 'dialog',
|
actionType: 'dialog',
|
||||||
dialog: detailDialog(),
|
dialog: detailDialog(id),
|
||||||
},*/
|
},
|
||||||
{
|
{
|
||||||
className: 'text-danger btn-deleted',
|
className: 'text-danger btn-deleted',
|
||||||
type: 'action',
|
type: 'action',
|
||||||
label: '删除',
|
label: '删除',
|
||||||
level: 'link',
|
level: 'link',
|
||||||
|
size: 'sm',
|
||||||
actionType: 'ajax',
|
actionType: 'ajax',
|
||||||
api: `get:${commonInfo.baseUrl}/chapter/remove/\${id}`,
|
api: `get:${commonInfo.baseUrl}/chapter/remove/\${id}`,
|
||||||
confirmText: '确认删除章节<span class="text-lg font-bold mx-2">${name}</span>?',
|
confirmText: '确认删除章节<span class="text-lg font-bold mx-2">${name}</span>?',
|
||||||
|
|||||||
@@ -107,12 +107,12 @@ function Bookshelf() {
|
|||||||
{
|
{
|
||||||
name: 'description',
|
name: 'description',
|
||||||
label: '描述',
|
label: '描述',
|
||||||
width: 250,
|
width: 500,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'source',
|
name: 'source',
|
||||||
label: '来源',
|
label: '来源',
|
||||||
width: 150,
|
width: 200,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'tags',
|
name: 'tags',
|
||||||
@@ -130,13 +130,14 @@ function Bookshelf() {
|
|||||||
{
|
{
|
||||||
type: 'operation',
|
type: 'operation',
|
||||||
label: '操作',
|
label: '操作',
|
||||||
width: 150,
|
width: 180,
|
||||||
fixed: 'right',
|
fixed: 'right',
|
||||||
buttons: [
|
buttons: [
|
||||||
{
|
{
|
||||||
type: 'action',
|
type: 'action',
|
||||||
label: '详情',
|
label: '详情',
|
||||||
level: 'link',
|
level: 'link',
|
||||||
|
size: 'sm',
|
||||||
onEvent: {
|
onEvent: {
|
||||||
click: {
|
click: {
|
||||||
actions: [
|
actions: [
|
||||||
@@ -159,11 +160,20 @@ function Bookshelf() {
|
|||||||
actionType: 'dialog',
|
actionType: 'dialog',
|
||||||
dialog: detailDialog(),
|
dialog: detailDialog(),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
type: 'action',
|
||||||
|
label: '导出',
|
||||||
|
level: 'link',
|
||||||
|
size: 'sm',
|
||||||
|
actionType: 'download',
|
||||||
|
api: `${commonInfo.baseUrl}/book/export/\${id}`,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
className: 'text-danger btn-deleted',
|
className: 'text-danger btn-deleted',
|
||||||
type: 'action',
|
type: 'action',
|
||||||
label: '删除',
|
label: '删除',
|
||||||
level: 'link',
|
level: 'link',
|
||||||
|
size: 'sm',
|
||||||
actionType: 'ajax',
|
actionType: 'ajax',
|
||||||
api: `get:${commonInfo.baseUrl}/book/remove/\${id}`,
|
api: `get:${commonInfo.baseUrl}/book/remove/\${id}`,
|
||||||
confirmText: '确认删除书籍<span class="text-lg font-bold mx-2">${name}</span>?',
|
confirmText: '确认删除书籍<span class="text-lg font-bold mx-2">${name}</span>?',
|
||||||
|
|||||||
@@ -1,6 +1,13 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {useParams} from 'react-router'
|
import {useParams} from 'react-router'
|
||||||
import {amisRender, commonInfo, crudCommonOptions, horizontalFormOptions, paginationTemplate} from '../../util/amis.tsx'
|
import {
|
||||||
|
amisRender,
|
||||||
|
commonInfo,
|
||||||
|
crudCommonOptions,
|
||||||
|
horizontalFormOptions,
|
||||||
|
paginationTemplate,
|
||||||
|
readOnlyDialogOptions,
|
||||||
|
} from '../../util/amis.tsx'
|
||||||
|
|
||||||
function Chapter() {
|
function Chapter() {
|
||||||
// const navigate = useNavigate()
|
// const navigate = useNavigate()
|
||||||
@@ -52,7 +59,7 @@ function Chapter() {
|
|||||||
},
|
},
|
||||||
...crudCommonOptions(),
|
...crudCommonOptions(),
|
||||||
...paginationTemplate(
|
...paginationTemplate(
|
||||||
undefined,
|
50,
|
||||||
undefined,
|
undefined,
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
@@ -62,9 +69,36 @@ function Chapter() {
|
|||||||
actionType: 'ajax',
|
actionType: 'ajax',
|
||||||
tooltip: '序号重排',
|
tooltip: '序号重排',
|
||||||
tooltipPlacement: 'top',
|
tooltipPlacement: 'top',
|
||||||
api: `get:${commonInfo.baseUrl}/line/generate_sequence/${id}`
|
api: `get:${commonInfo.baseUrl}/line/generate_sequence/${id}`,
|
||||||
}
|
confirmText: '确认重排序号?',
|
||||||
]
|
confirmTitle: '序号重拍',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'action',
|
||||||
|
label: '',
|
||||||
|
icon: 'fa fa-book-open-reader',
|
||||||
|
actionType: 'dialog',
|
||||||
|
tooltip: '全文阅读',
|
||||||
|
tooltipPlacement: 'top',
|
||||||
|
dialog: {
|
||||||
|
title: '全文查看',
|
||||||
|
size: 'md',
|
||||||
|
...readOnlyDialogOptions(),
|
||||||
|
body: {
|
||||||
|
type: 'service',
|
||||||
|
size: 'none',
|
||||||
|
api: `${commonInfo.baseUrl}/chapter/content/${id}`,
|
||||||
|
body: {
|
||||||
|
type: 'markdown',
|
||||||
|
value: '${detail}',
|
||||||
|
options: {
|
||||||
|
breaks: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
),
|
),
|
||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
@@ -125,6 +159,7 @@ function Chapter() {
|
|||||||
type: 'action',
|
type: 'action',
|
||||||
label: '删除',
|
label: '删除',
|
||||||
level: 'link',
|
level: 'link',
|
||||||
|
size: 'sm',
|
||||||
actionType: 'ajax',
|
actionType: 'ajax',
|
||||||
api: `get:${commonInfo.baseUrl}/line/remove/\${id}`,
|
api: `get:${commonInfo.baseUrl}/line/remove/\${id}`,
|
||||||
confirmText: '确认删除行?',
|
confirmText: '确认删除行?',
|
||||||
|
|||||||
37
pom.xml
37
pom.xml
@@ -16,11 +16,13 @@
|
|||||||
|
|
||||||
<spring-boot.version>3.5.0</spring-boot.version>
|
<spring-boot.version>3.5.0</spring-boot.version>
|
||||||
<spring-cloud.version>2025.0.0</spring-cloud.version>
|
<spring-cloud.version>2025.0.0</spring-cloud.version>
|
||||||
|
<spring-ai.version>1.1.0</spring-ai.version>
|
||||||
<hibernate.version>6.6.15.Final</hibernate.version>
|
<hibernate.version>6.6.15.Final</hibernate.version>
|
||||||
<querydsl.version>7.0</querydsl.version>
|
<querydsl.version>7.0</querydsl.version>
|
||||||
|
|
||||||
<hutool.version>5.8.39</hutool.version>
|
<hutool.version>5.8.39</hutool.version>
|
||||||
<liteflow.version>2.15.0</liteflow.version>
|
<liteflow.version>2.15.0</liteflow.version>
|
||||||
|
<selenium.version>4.38.1</selenium.version>
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
@@ -37,6 +39,10 @@
|
|||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.ai</groupId>
|
||||||
|
<artifactId>spring-ai-starter-model-openai</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.projectlombok</groupId>
|
<groupId>org.projectlombok</groupId>
|
||||||
@@ -44,20 +50,36 @@
|
|||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>cn.hutool</groupId>
|
<groupId>cn.hutool</groupId>
|
||||||
<artifactId>hutool-core</artifactId>
|
<artifactId>hutool-all</artifactId>
|
||||||
<version>${hutool.version}</version>
|
<version>${hutool.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>cn.hutool</groupId>
|
||||||
|
<artifactId>hutool-ai</artifactId>
|
||||||
|
<version>${hutool.version}</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.yomahub</groupId>
|
<groupId>com.yomahub</groupId>
|
||||||
<artifactId>liteflow-spring-boot-starter</artifactId>
|
<artifactId>liteflow-spring-boot-starter</artifactId>
|
||||||
<version>${liteflow.version}</version>
|
<version>${liteflow.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.seleniumhq.selenium</groupId>
|
||||||
|
<artifactId>selenium-java</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.h2database</groupId>
|
<groupId>com.h2database</groupId>
|
||||||
<artifactId>h2</artifactId>
|
<artifactId>h2</artifactId>
|
||||||
<scope>runtime</scope>
|
<scope>runtime</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.mysql</groupId>
|
||||||
|
<artifactId>mysql-connector-j</artifactId>
|
||||||
|
<scope>runtime</scope>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<dependencyManagement>
|
<dependencyManagement>
|
||||||
@@ -77,6 +99,19 @@
|
|||||||
<type>pom</type>
|
<type>pom</type>
|
||||||
<scope>import</scope>
|
<scope>import</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.ai</groupId>
|
||||||
|
<artifactId>spring-ai-bom</artifactId>
|
||||||
|
<version>${spring-ai.version}</version>
|
||||||
|
<type>pom</type>
|
||||||
|
<scope>import</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.seleniumhq.selenium</groupId>
|
||||||
|
<artifactId>selenium-dependencies-bom</artifactId>
|
||||||
|
<version>${selenium.version}</version>
|
||||||
|
<type>pom</type>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
</dependencyManagement>
|
</dependencyManagement>
|
||||||
|
|
||||||
|
|||||||
@@ -1,23 +1,10 @@
|
|||||||
package com.lanyuanxiaoyao.bookstore;
|
package com.lanyuanxiaoyao.bookstore;
|
||||||
|
|
||||||
import cn.hutool.core.lang.Tuple;
|
|
||||||
import cn.hutool.core.text.csv.CsvUtil;
|
|
||||||
import cn.hutool.core.util.NumberUtil;
|
|
||||||
import cn.hutool.core.util.ObjectUtil;
|
|
||||||
import com.blinkfox.fenix.EnableFenix;
|
import com.blinkfox.fenix.EnableFenix;
|
||||||
import com.lanyuanxiaoyao.bookstore.entity.Chapter;
|
|
||||||
import com.lanyuanxiaoyao.bookstore.entity.Line;
|
|
||||||
import com.lanyuanxiaoyao.bookstore.service.BookService;
|
|
||||||
import com.lanyuanxiaoyao.bookstore.service.ChapterService;
|
|
||||||
import jakarta.annotation.Resource;
|
|
||||||
import java.io.FileNotFoundException;
|
|
||||||
import java.io.FileReader;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
import org.springframework.boot.SpringApplication;
|
import org.springframework.boot.SpringApplication;
|
||||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||||
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
|
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 启动类
|
* 启动类
|
||||||
@@ -33,44 +20,4 @@ public class BookStoreApplication {
|
|||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
SpringApplication.run(BookStoreApplication.class, args);
|
SpringApplication.run(BookStoreApplication.class, args);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Resource
|
|
||||||
private BookService bookService;
|
|
||||||
@Resource
|
|
||||||
private ChapterService chapterService;
|
|
||||||
|
|
||||||
@Transactional(rollbackFor = Throwable.class)
|
|
||||||
// @EventListener(ApplicationReadyEvent.class)
|
|
||||||
public void loadOldData() throws FileNotFoundException {
|
|
||||||
var reader = CsvUtil.getReader(new FileReader("C:\\Users\\lanyuanxiaoyao\\Result_6.csv"));
|
|
||||||
var rows = reader.stream()
|
|
||||||
.map(row -> new Row(Long.parseLong(row.get(0)), row.get(1), Long.parseLong(row.get(2)), row.get(3)))
|
|
||||||
.toList();
|
|
||||||
var book = bookService.detailOrThrow(3602572744994816L);
|
|
||||||
rows.stream()
|
|
||||||
.map(row -> new Tuple(row.chapterSequence(), row.chapterTitle()))
|
|
||||||
.distinct()
|
|
||||||
.forEach(tuple -> {
|
|
||||||
var chapter = new Chapter();
|
|
||||||
chapter.setSequence(NumberUtil.toDouble(tuple.get(0)));
|
|
||||||
chapter.setName(tuple.get(1));
|
|
||||||
chapter.setBook(book);
|
|
||||||
|
|
||||||
var lines = rows.stream()
|
|
||||||
.filter(row -> ObjectUtil.equals(row.chapterSequence(), tuple.get(0)) && ObjectUtil.equals(row.chapterTitle(), tuple.get(1)))
|
|
||||||
.map(row -> {
|
|
||||||
var line = new Line();
|
|
||||||
line.setSequence(NumberUtil.toDouble(row.lineSequence()));
|
|
||||||
line.setText(row.lineText());
|
|
||||||
line.setChapter(chapter);
|
|
||||||
return line;
|
|
||||||
})
|
|
||||||
.collect(Collectors.toSet());
|
|
||||||
chapter.setContent(lines);
|
|
||||||
chapterService.save(chapter);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public record Row(long chapterSequence, String chapterTitle, long lineSequence, String lineText) {
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,25 @@
|
|||||||
package com.lanyuanxiaoyao.bookstore.controller;
|
package com.lanyuanxiaoyao.bookstore.controller;
|
||||||
|
|
||||||
|
import cn.hutool.core.io.IoUtil;
|
||||||
|
import cn.hutool.core.util.StrUtil;
|
||||||
|
import cn.hutool.core.util.URLUtil;
|
||||||
import com.lanyuanxiaoyao.bookstore.entity.Book;
|
import com.lanyuanxiaoyao.bookstore.entity.Book;
|
||||||
|
import com.lanyuanxiaoyao.bookstore.entity.Chapter;
|
||||||
|
import com.lanyuanxiaoyao.bookstore.entity.Line;
|
||||||
import com.lanyuanxiaoyao.bookstore.entity.vo.Option;
|
import com.lanyuanxiaoyao.bookstore.entity.vo.Option;
|
||||||
import com.lanyuanxiaoyao.bookstore.service.BookService;
|
import com.lanyuanxiaoyao.bookstore.service.BookService;
|
||||||
import com.lanyuanxiaoyao.service.template.controller.SimpleControllerSupport;
|
import com.lanyuanxiaoyao.service.template.controller.SimpleControllerSupport;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.Comparator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
@@ -29,6 +39,39 @@ public class BookController extends SimpleControllerSupport<Book, BookController
|
|||||||
return bookService.tags();
|
return bookService.tags();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("export/{book_id}")
|
||||||
|
public void export(@PathVariable("book_id") Long bookId, HttpServletResponse response) throws IOException {
|
||||||
|
var book = bookService.detailOrThrow(bookId);
|
||||||
|
var result = new StringBuilder();
|
||||||
|
result.append(StrUtil.format("""
|
||||||
|
书名:{}
|
||||||
|
作者:{}
|
||||||
|
简介:{}
|
||||||
|
来源:{}
|
||||||
|
|
||||||
|
""", book.getName(), book.getAuthor(), book.getDescription(), book.getSource()));
|
||||||
|
book.getChapters()
|
||||||
|
.stream()
|
||||||
|
.sorted(Comparator.comparing(Chapter::getSequence))
|
||||||
|
.forEach(chapter -> {
|
||||||
|
result.append(StrUtil.format("第{}章 {}", chapter.getSequence(), chapter.getName()));
|
||||||
|
result.append("\n\n");
|
||||||
|
chapter.getContent()
|
||||||
|
.stream()
|
||||||
|
.sorted(Comparator.comparing(Line::getSequence))
|
||||||
|
.forEach(line -> {
|
||||||
|
result.append(line.getText());
|
||||||
|
result.append("\n");
|
||||||
|
});
|
||||||
|
result.append("\n\n");
|
||||||
|
});
|
||||||
|
response.setHeader("Access-Control-Expose-Headers", "Content-Type");
|
||||||
|
response.setHeader("Content-Type", "text/plain");
|
||||||
|
response.setHeader("Access-Control-Expose-Headers", "Content-Disposition");
|
||||||
|
response.setHeader("Content-Disposition", StrUtil.format("attachment; filename={}", URLUtil.encodeAll(StrUtil.format("{}.txt", book.getName()))));
|
||||||
|
IoUtil.copy(new ByteArrayInputStream(result.toString().getBytes()), response.getOutputStream());
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Function<SaveItem, Book> saveItemMapper() {
|
protected Function<SaveItem, Book> saveItemMapper() {
|
||||||
return item -> {
|
return item -> {
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package com.lanyuanxiaoyao.bookstore.controller;
|
package com.lanyuanxiaoyao.bookstore.controller;
|
||||||
|
|
||||||
|
import cn.hutool.core.util.ObjectUtil;
|
||||||
import com.lanyuanxiaoyao.bookstore.entity.Chapter;
|
import com.lanyuanxiaoyao.bookstore.entity.Chapter;
|
||||||
|
import com.lanyuanxiaoyao.bookstore.entity.Line;
|
||||||
import com.lanyuanxiaoyao.bookstore.entity.vo.Option;
|
import com.lanyuanxiaoyao.bookstore.entity.vo.Option;
|
||||||
import com.lanyuanxiaoyao.bookstore.service.BookService;
|
import com.lanyuanxiaoyao.bookstore.service.BookService;
|
||||||
import com.lanyuanxiaoyao.bookstore.service.ChapterService;
|
import com.lanyuanxiaoyao.bookstore.service.ChapterService;
|
||||||
@@ -8,11 +10,15 @@ import com.lanyuanxiaoyao.bookstore.service.LineService;
|
|||||||
import com.lanyuanxiaoyao.service.template.controller.GlobalResponse;
|
import com.lanyuanxiaoyao.service.template.controller.GlobalResponse;
|
||||||
import com.lanyuanxiaoyao.service.template.controller.SimpleControllerSupport;
|
import com.lanyuanxiaoyao.service.template.controller.SimpleControllerSupport;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.Comparator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestBody;
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
@@ -43,7 +49,7 @@ public class ChapterController extends SimpleControllerSupport<Chapter, ChapterC
|
|||||||
public GlobalResponse<Object> saveWithContent(@RequestBody SaveWithContentItem item) {
|
public GlobalResponse<Object> saveWithContent(@RequestBody SaveWithContentItem item) {
|
||||||
if (SaveWithContentItem.Mode.CREATE.equals(item.mode())) {
|
if (SaveWithContentItem.Mode.CREATE.equals(item.mode())) {
|
||||||
var chapter = new Chapter();
|
var chapter = new Chapter();
|
||||||
chapter.setSequence(chapterService.latestSequence(item.bookId()));
|
chapter.setSequence(chapterService.latestSequence(item.bookId()) + 1);
|
||||||
chapter.setName(item.name());
|
chapter.setName(item.name());
|
||||||
chapter.setDescription(item.description());
|
chapter.setDescription(item.description());
|
||||||
chapter.setBook(bookService.detailOrThrow(item.bookId()));
|
chapter.setBook(bookService.detailOrThrow(item.bookId()));
|
||||||
@@ -62,6 +68,18 @@ public class ChapterController extends SimpleControllerSupport<Chapter, ChapterC
|
|||||||
return GlobalResponse.responseSuccess();
|
return GlobalResponse.responseSuccess();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("content/{chapter_id}")
|
||||||
|
public GlobalResponse<Map<String, Object>> content(@PathVariable("chapter_id") Long chapterId) {
|
||||||
|
var chapter = chapterService.detailOrThrow(chapterId);
|
||||||
|
return GlobalResponse.responseDetailData(
|
||||||
|
chapter.getContent()
|
||||||
|
.stream()
|
||||||
|
.sorted(Comparator.comparing(Line::getSequence))
|
||||||
|
.map(Line::getText)
|
||||||
|
.collect(Collectors.joining("\n"))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping("generate_sequence")
|
@GetMapping("generate_sequence")
|
||||||
public GlobalResponse<Object> generateSequence() {
|
public GlobalResponse<Object> generateSequence() {
|
||||||
chapterService.generateSequence();
|
chapterService.generateSequence();
|
||||||
@@ -73,7 +91,7 @@ public class ChapterController extends SimpleControllerSupport<Chapter, ChapterC
|
|||||||
return item -> {
|
return item -> {
|
||||||
var chapter = new Chapter();
|
var chapter = new Chapter();
|
||||||
chapter.setId(item.id());
|
chapter.setId(item.id());
|
||||||
chapter.setSequence(chapterService.latestSequence(item.bookId()));
|
chapter.setSequence(ObjectUtil.defaultIfNull(item.sequence(), chapterService.latestSequence(item.bookId()) + 1));
|
||||||
chapter.setName(item.name());
|
chapter.setName(item.name());
|
||||||
chapter.setDescription(item.description());
|
chapter.setDescription(item.description());
|
||||||
chapter.setBook(bookService.detailOrThrow(item.bookId()));
|
chapter.setBook(bookService.detailOrThrow(item.bookId()));
|
||||||
@@ -105,6 +123,7 @@ public class ChapterController extends SimpleControllerSupport<Chapter, ChapterC
|
|||||||
public record SaveItem(
|
public record SaveItem(
|
||||||
Long id,
|
Long id,
|
||||||
Long bookId,
|
Long bookId,
|
||||||
|
Double sequence,
|
||||||
String name,
|
String name,
|
||||||
String description
|
String description
|
||||||
) {
|
) {
|
||||||
@@ -118,10 +137,6 @@ public class ChapterController extends SimpleControllerSupport<Chapter, ChapterC
|
|||||||
Mode mode,
|
Mode mode,
|
||||||
String content
|
String content
|
||||||
) {
|
) {
|
||||||
public SaveItem toSaveItem() {
|
|
||||||
return new SaveItem(null, bookId, name, description);
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum Mode {
|
public enum Mode {
|
||||||
CREATE, OVERRIDE
|
CREATE, OVERRIDE
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ public class Book extends SimpleEntity {
|
|||||||
@Column(nullable = false)
|
@Column(nullable = false)
|
||||||
private String name;
|
private String name;
|
||||||
private String author;
|
private String author;
|
||||||
|
@Column(columnDefinition = "text")
|
||||||
private String description;
|
private String description;
|
||||||
private String source;
|
private String source;
|
||||||
@ElementCollection(fetch = FetchType.EAGER)
|
@ElementCollection(fetch = FetchType.EAGER)
|
||||||
|
|||||||
@@ -40,7 +40,6 @@ import org.springframework.data.jpa.domain.support.AuditingEntityListener;
|
|||||||
public class Chapter extends SimpleEntity {
|
public class Chapter extends SimpleEntity {
|
||||||
@Column(nullable = false)
|
@Column(nullable = false)
|
||||||
private Double sequence;
|
private Double sequence;
|
||||||
@Column(nullable = false)
|
|
||||||
private String name;
|
private String name;
|
||||||
private String description;
|
private String description;
|
||||||
|
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ public class Line extends SimpleEntity {
|
|||||||
@Lob
|
@Lob
|
||||||
@Basic(fetch = FetchType.LAZY)
|
@Basic(fetch = FetchType.LAZY)
|
||||||
@ToString.Exclude
|
@ToString.Exclude
|
||||||
@Column(nullable = false)
|
@Column(nullable = false, columnDefinition = "longtext")
|
||||||
private String text;
|
private String text;
|
||||||
|
|
||||||
@ManyToOne(cascade = CascadeType.DETACH, fetch = FetchType.LAZY)
|
@ManyToOne(cascade = CascadeType.DETACH, fetch = FetchType.LAZY)
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
package com.lanyuanxiaoyao.bookstore.helper;
|
||||||
|
|
||||||
|
import cn.hutool.core.util.ObjectUtil;
|
||||||
|
import cn.hutool.core.util.StrUtil;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 优化工具
|
||||||
|
*
|
||||||
|
* @author lanyuanxiaoyao
|
||||||
|
* @version 20250929
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
public class OptimiseHelper {
|
||||||
|
public static String optimize(String text) {
|
||||||
|
// 移除空行
|
||||||
|
text = StrUtil.trimToNull(text);
|
||||||
|
if (ObjectUtil.isNull(text)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 英文全角字符转换为半角字符
|
||||||
|
text = halfWidth(text);
|
||||||
|
|
||||||
|
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String halfWidth(String text) {
|
||||||
|
var builder = new StringBuilder();
|
||||||
|
for (var c : text.toCharArray()) {
|
||||||
|
// 检查是否为全角数字 (U+FF10 到 U+FF19)
|
||||||
|
if (c >= 0xFF10 && c <= 0xFF19) {
|
||||||
|
// 对应的半角数字是 (c - 0xFEE0)
|
||||||
|
builder.append((char) (c - 0xFEE0));
|
||||||
|
}
|
||||||
|
// 检查是否为全角大写字母 (U+FF21 到 U+FF3A)
|
||||||
|
else if (c >= 0xFF21 && c <= 0xFF3A) {
|
||||||
|
// 对应的半角字母是 (c - 0xFEE0)
|
||||||
|
builder.append((char) (c - 0xFEE0));
|
||||||
|
}
|
||||||
|
// 检查是否为全角小写字母 (U+FF41 到 U+FF5A)
|
||||||
|
else if (c >= 0xFF41 && c <= 0xFF5A) {
|
||||||
|
// 对应的半角字母是 (c - 0xFEE0)
|
||||||
|
builder.append((char) (c - 0xFEE0));
|
||||||
|
}
|
||||||
|
// 非全角字母和数字字符直接添加
|
||||||
|
else {
|
||||||
|
builder.append(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return builder.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,7 +8,7 @@ import org.springframework.stereotype.Repository;
|
|||||||
|
|
||||||
@Repository
|
@Repository
|
||||||
public interface ChapterRepository extends SimpleRepository<Chapter> {
|
public interface ChapterRepository extends SimpleRepository<Chapter> {
|
||||||
@Query("select max(chapter.sequence) from Chapter chapter")
|
@Query("select max(chapter.sequence) from Chapter chapter where chapter.book.id = ?1")
|
||||||
Optional<Double> findMaxSequence(Long bookId);
|
Optional<Double> findMaxSequence(Long bookId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,12 +2,12 @@ package com.lanyuanxiaoyao.bookstore.service;
|
|||||||
|
|
||||||
import cn.hutool.core.util.NumberUtil;
|
import cn.hutool.core.util.NumberUtil;
|
||||||
import com.lanyuanxiaoyao.bookstore.entity.Chapter;
|
import com.lanyuanxiaoyao.bookstore.entity.Chapter;
|
||||||
|
import com.lanyuanxiaoyao.bookstore.entity.QChapter;
|
||||||
import com.lanyuanxiaoyao.bookstore.entity.vo.Option;
|
import com.lanyuanxiaoyao.bookstore.entity.vo.Option;
|
||||||
import com.lanyuanxiaoyao.bookstore.repository.ChapterRepository;
|
import com.lanyuanxiaoyao.bookstore.repository.ChapterRepository;
|
||||||
import com.lanyuanxiaoyao.service.template.service.SimpleServiceSupport;
|
import com.lanyuanxiaoyao.service.template.service.SimpleServiceSupport;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.data.domain.Sort;
|
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@@ -32,7 +32,7 @@ public class ChapterService extends SimpleServiceSupport<Chapter> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void generateSequence() {
|
public void generateSequence() {
|
||||||
var chapters = chapterRepository.findAll(Sort.by(Sort.Direction.ASC, Chapter.Fields.sequence));
|
var chapters = chapterRepository.findAll(QChapter.chapter.sequence.asc());
|
||||||
for (int index = 0; index < chapters.size(); index++) {
|
for (int index = 0; index < chapters.size(); index++) {
|
||||||
chapters.get(index).setSequence(NumberUtil.toDouble(index));
|
chapters.get(index).setSequence(NumberUtil.toDouble(index));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package com.lanyuanxiaoyao.bookstore.service;
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 书籍下载
|
||||||
|
*
|
||||||
|
* @author lanyuanxiaoyao
|
||||||
|
* @version 20251031
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
public class CrawlerService {
|
||||||
|
}
|
||||||
@@ -9,16 +9,23 @@ spring:
|
|||||||
async:
|
async:
|
||||||
request-timeout: 3600000
|
request-timeout: 3600000
|
||||||
datasource:
|
datasource:
|
||||||
url: jdbc:h2:file:./bookstore;DB_CLOSE_ON_EXIT=TRUE
|
url: jdbc:mysql://mysql.lanyuanxiaoyao.com:43780/bookstore?useUnicode=true&characterEncoding=utf8&useSSL=false
|
||||||
username: bookstore
|
username: bookstore
|
||||||
password: bookstore
|
password: EzSn+RZ*x2&fHFh9kC+H
|
||||||
driver-class-name: org.h2.Driver
|
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||||
jpa:
|
jpa:
|
||||||
generate-ddl: false
|
generate-ddl: false
|
||||||
hibernate:
|
hibernate:
|
||||||
ddl-auto: update
|
ddl-auto: update
|
||||||
main:
|
main:
|
||||||
banner-mode: off
|
banner-mode: off
|
||||||
|
ai:
|
||||||
|
openai:
|
||||||
|
base-url: https://openrouter.ai/api/v1
|
||||||
|
api-key: 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
|
||||||
|
chat:
|
||||||
|
options:
|
||||||
|
model: "x-ai/grok-4.1-fast:free"
|
||||||
fenix:
|
fenix:
|
||||||
print-banner: false
|
print-banner: false
|
||||||
liteflow:
|
liteflow:
|
||||||
|
|||||||
@@ -0,0 +1,99 @@
|
|||||||
|
package com.lanyuanxiaoyao.bookstore;
|
||||||
|
|
||||||
|
import cn.hutool.core.thread.ThreadUtil;
|
||||||
|
import cn.hutool.core.util.StrUtil;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.openqa.selenium.By;
|
||||||
|
import org.openqa.selenium.chrome.ChromeDriver;
|
||||||
|
import org.openqa.selenium.chrome.ChromeDriverService;
|
||||||
|
import org.openqa.selenium.chrome.ChromeOptions;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author lanyuanxiaoyao
|
||||||
|
* @version 20251031
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
public class BookCrawlerTest {
|
||||||
|
public static void main(String[] args) throws IOException {
|
||||||
|
ChromeDriver driver = null;
|
||||||
|
try {
|
||||||
|
driver = new ChromeDriver(
|
||||||
|
new ChromeDriverService.Builder()
|
||||||
|
.usingDriverExecutable(new File("/Users/lanyuanxiaoyao/SynologyDrive/tools/chromium/134/macOS-1345775/chromedriver"))
|
||||||
|
.build(),
|
||||||
|
new ChromeOptions()
|
||||||
|
.setBinary(new File("/Users/lanyuanxiaoyao/SynologyDrive/tools/chromium/134/macOS-1345775/Chromium.app/Contents/MacOS/Chromium"))
|
||||||
|
.addArguments(
|
||||||
|
// 允许不安全的域名
|
||||||
|
"--allow-insecure-localhost",
|
||||||
|
// 禁用GPU渲染,headLess 模式用不上
|
||||||
|
"--disable-gpu",
|
||||||
|
// 禁用音频输出
|
||||||
|
"--disable-audio-output",
|
||||||
|
// 禁用错误页自动重新刷新
|
||||||
|
"--disable-auto-reload",
|
||||||
|
// 禁用默认应用加载
|
||||||
|
"--disable-default-apps",
|
||||||
|
// 禁用浏览器扩展
|
||||||
|
"--disable-extensions",
|
||||||
|
// 禁用日志
|
||||||
|
"--disable-logging",
|
||||||
|
// 禁用通知
|
||||||
|
"--disable-notifications",
|
||||||
|
// 禁用远程字体
|
||||||
|
"--disable-remote-fonts",
|
||||||
|
// 禁用弹出窗口
|
||||||
|
"--disable-popup-blocking",
|
||||||
|
// 禁用同步
|
||||||
|
"--disable-sync",
|
||||||
|
// 禁用沙盒
|
||||||
|
"--no-sandbox",
|
||||||
|
// 禁用声音
|
||||||
|
"--mute-audio",
|
||||||
|
// 禁止图片显示
|
||||||
|
"blink-settings=imagesEnabled=false"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
var articleUrl = "https://www.alicesw.com/novel/32007.html";
|
||||||
|
driver.get(articleUrl);
|
||||||
|
var contextUrl = driver.findElement(By.xpath("//div[@class='book_newchap']//a[contains(text(),'查看所有章节')]")).getDomProperty("href");
|
||||||
|
if (StrUtil.isBlank(contextUrl)) {
|
||||||
|
throw new RuntimeException("获取目录页链接失败");
|
||||||
|
}
|
||||||
|
driver.get(contextUrl);
|
||||||
|
var chapterItems = driver.findElements(By.cssSelector("ul.mulu_list > li > a"))
|
||||||
|
.stream()
|
||||||
|
.map(element -> element.getDomProperty("href"))
|
||||||
|
.toList();
|
||||||
|
for (var index = 0; index < chapterItems.size(); index++) {
|
||||||
|
var chapterUrl = chapterItems.get(index);
|
||||||
|
if (StrUtil.isBlank(chapterUrl)) {
|
||||||
|
throw new RuntimeException("获取章节链接失败: " + chapterUrl);
|
||||||
|
}
|
||||||
|
driver.get(chapterUrl);
|
||||||
|
var text = driver.findElement(By.cssSelector(".read-content")).getText();
|
||||||
|
log.info(text);
|
||||||
|
|
||||||
|
var title = driver.getTitle();
|
||||||
|
if (StrUtil.isBlank(title)) {
|
||||||
|
title = String.valueOf(index);
|
||||||
|
}
|
||||||
|
var filename = StrUtil.format("{}.txt", title.replaceAll("\\s", "_"));
|
||||||
|
var targetFile = Path.of("out2", filename);
|
||||||
|
Files.deleteIfExists(targetFile);
|
||||||
|
Files.createFile(targetFile);
|
||||||
|
Files.writeString(targetFile, text);
|
||||||
|
|
||||||
|
ThreadUtil.safeSleep(2000);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (driver != null) {
|
||||||
|
driver.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
package com.lanyuanxiaoyao.bookstore;
|
||||||
|
|
||||||
|
import cn.hutool.core.io.FileUtil;
|
||||||
|
import cn.hutool.core.util.ObjectUtil;
|
||||||
|
import cn.hutool.core.util.StrUtil;
|
||||||
|
import cn.hutool.db.dialect.impl.MysqlDialect;
|
||||||
|
import com.lanyuanxiaoyao.service.template.helper.SnowflakeIdGenerator;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.sql.DriverManager;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新章节
|
||||||
|
*
|
||||||
|
* @author lanyuanxiaoyao
|
||||||
|
* @version 20251031
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
public class UpdateIntoDatabase {
|
||||||
|
public static void main(String[] args) throws IOException, SQLException, ClassNotFoundException {
|
||||||
|
var chapters = new ArrayList<Chapter>();
|
||||||
|
Files.list(Path.of("out"))
|
||||||
|
.sorted(Comparator.comparing(path -> FileUtil.lastModifiedTime(path.toFile())))
|
||||||
|
.forEach(path -> {
|
||||||
|
try {
|
||||||
|
chapters.add(new Chapter(
|
||||||
|
path.getFileName().toString(),
|
||||||
|
Files.readString(path)
|
||||||
|
));
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Class.forName("com.mysql.cj.jdbc.Driver");
|
||||||
|
try (
|
||||||
|
var connection = DriverManager.getConnection(
|
||||||
|
"jdbc:mysql://mysql.lanyuanxiaoyao.com:43780/bookstore?useUnicode=true&characterEncoding=utf8&useSSL=false",
|
||||||
|
"bookstore",
|
||||||
|
"EzSn+RZ*x2&fHFh9kC+H"
|
||||||
|
)
|
||||||
|
/*var ds = new SimpleDataSource(
|
||||||
|
"jdbc:mysql://mysql.lanyuanxiaoyao.com:43780/bookstore?useUnicode=true&characterEncoding=utf8&useSSL=false",
|
||||||
|
"bookstore",
|
||||||
|
"EzSn+RZ*x2&fHFh9kC+H",
|
||||||
|
"com.mysql.cj.jdbc.Driver"
|
||||||
|
)*/
|
||||||
|
) {
|
||||||
|
var dialect = new MysqlDialect();
|
||||||
|
for (int index = 0; index < chapters.size(); index++) {
|
||||||
|
var chapter = chapters.get(index);
|
||||||
|
log.info("Chapter: {}", chapter.title());
|
||||||
|
var now = LocalDateTime.now();
|
||||||
|
var chapterId = SnowflakeIdGenerator.Snowflake.next();
|
||||||
|
/*Db.use(ds, dialect).insert(
|
||||||
|
Entity.create("bookstore_chapter")
|
||||||
|
.set("id", chapterId)
|
||||||
|
.set("created_time", now)
|
||||||
|
.set("modified_time", now)
|
||||||
|
.set("sequence", index)
|
||||||
|
.set("book_id", 3608359126886400L)
|
||||||
|
);*/
|
||||||
|
try (var statement = connection.prepareStatement("insert into bookstore_chapter(id,created_time,modified_time,sequence,book_id) values(?,current_timestamp(),current_timestamp(),?,?)")) {
|
||||||
|
statement.setLong(1, chapterId);
|
||||||
|
statement.setDouble(2, index);
|
||||||
|
statement.setLong(3, 3608359126886400L);
|
||||||
|
statement.execute();
|
||||||
|
}
|
||||||
|
var lines = StrUtil.split(chapter.content, "\n")
|
||||||
|
.stream()
|
||||||
|
.map(StrUtil::trimToNull)
|
||||||
|
.filter(ObjectUtil::isNotNull)
|
||||||
|
.toList();
|
||||||
|
// var entities = new ArrayList<Entity>();
|
||||||
|
for (int lineIndex = 0; lineIndex < lines.size(); lineIndex++) {
|
||||||
|
/*entities.add(
|
||||||
|
Entity.create("bookstore_line")
|
||||||
|
.set("id", SnowflakeIdGenerator.Snowflake.next())
|
||||||
|
.set("created_time", now)
|
||||||
|
.set("modified_time", now)
|
||||||
|
.set("sequence", lineIndex)
|
||||||
|
.set("text", lines.get(lineIndex))
|
||||||
|
.set("chapter_id", chapterId)
|
||||||
|
);*/
|
||||||
|
try (var statement = connection.prepareStatement("insert into bookstore_line(id,created_time,modified_time,sequence,text,chapter_id) values(?,current_timestamp(),current_timestamp(),?,?,?)")) {
|
||||||
|
statement.setLong(1, SnowflakeIdGenerator.Snowflake.next());
|
||||||
|
statement.setDouble(2, lineIndex);
|
||||||
|
statement.setString(3, lines.get(lineIndex));
|
||||||
|
statement.setLong(4, chapterId);
|
||||||
|
statement.execute();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Db.use(ds, dialect).insert(entities);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*var response = AIUtil.chat(
|
||||||
|
new AIConfigBuilder(ModelName.OPENAI.getValue())
|
||||||
|
.setApiUrl("http://127.0.0.1:30000")
|
||||||
|
.setApiKey("*XMySqV%>hR&v>>g*NwCs3tpQ5FVMFEF2VHVTj<MYQd$&@$sY7CgqNyea4giJi4")
|
||||||
|
.setModel("")
|
||||||
|
.setTimout(1000 * 60 * 5)
|
||||||
|
.build(),
|
||||||
|
"""
|
||||||
|
—————
|
||||||
|
上述是小说一个章节的内容,需要你为章节拟2句金庸武侠风格的七言绝句作为章节标题,标题在精确概括章节内容外也需要充满文学性、浪漫性,直接输出标题文字,除了标题外严禁输出任何无关的文字,禁止输出任何markdown格式
|
||||||
|
"""
|
||||||
|
);
|
||||||
|
log.info("response: {}", response);*/
|
||||||
|
}
|
||||||
|
|
||||||
|
public record Chapter(String title, String content) {
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user