diff --git a/src/components/article/article.module.scss b/src/components/article/article.module.scss
index 349a72a..1a06d04 100644
--- a/src/components/article/article.module.scss
+++ b/src/components/article/article.module.scss
@@ -1,5 +1,5 @@
.blockContainer {
- @apply flex mb-5;
+ @apply flex mb-5;
}
.block {
@@ -14,6 +14,10 @@
}
}
+.blockFist {
+ @apply p-0 border-0 !important;
+}
+
.blockItem {
}
@@ -21,40 +25,61 @@
.group {
}
-.image {
- @apply border border-blue-200 p-2 flex-1 rounded focus:border-blue-200;
- min-height: 100px;
- &:hover{
- @apply border-blue-500;
- }
- :global{
- .ant-upload-wrapper{
+.imageList {
+ @apply grid grid-cols-4 gap-4 p-3 border border-blue-200;
+ :global {
+ .ant-upload-wrapper {
display: block;
border: none;
padding: 0;
}
- .ant-upload{
+
+ .ant-upload {
display: block;
}
+
+ img {
+ @apply block m-0;
+ max-width: 100%;
+ height: 100px;
+ object-fit: contain;
+ padding: 2px;
+ }
}
}
+.image {
+ @apply rounded bg-gray-100;
+ height: 100px;
+
+ &:hover {
+ @apply border-blue-500;
+ }
+}
+.imageDelete{
+ @apply absolute flex items-center justify-center p-0.5 w-[22px] h-[22px] rounded-full border border-red-500 text-red-500 cursor-pointer z-10;
+ right:-10px;
+ top:-10px;
+ font-size: 14px;
+ &:hover{
+ @apply text-white bg-red-500;
+ }
+}
.uploadImage {
- @apply flex justify-center items-center relative;
- img {
- display: block;
- max-width: 100%;
- max-height: 200px;
+ @apply flex justify-center items-center relative h-[100px] text-gray-400;
+
+ .uploadTips {
+ @apply absolute inset-0 cursor-pointer opacity-0 transition rounded flex items-center justify-center bg-black/20 text-white;
}
- .uploadTips{
- @apply absolute inset-0 cursor-pointer opacity-0 rounded flex items-center justify-center bg-black/50 text-white;
- }
- .imagePlaceholder{
+
+ .imagePlaceholder {
@apply flex items-center justify-center;
height: 100px;
}
- &:hover{
- .uploadTips{
+
+ &:hover {
+ @apply bg-gray-100 cursor-pointer rounded text-blue-500;
+ .uploadTips {
@apply opacity-100;
}
}
@@ -62,10 +87,11 @@
.text {
@apply border border-blue-200 overflow-hidden flex-1 rounded focus:border-blue-200 transition;
- &:hover{
+ &:hover {
@apply border-blue-500;
}
- &:focus-within{
+
+ &:focus-within {
@apply border-blue-500 shadow-md;
}
}
diff --git a/src/components/article/block.tsx b/src/components/article/block.tsx
index 65882e6..1553d3e 100644
--- a/src/components/article/block.tsx
+++ b/src/components/article/block.tsx
@@ -1,24 +1,56 @@
import React from "react";
import clsx from "clsx";
+import {Popconfirm, Space} from "antd";
-import {IconAdd, IconAddImage, IconAddText, IconDelete} from "@/components/icons";
-import {BlockImage, BlockText} from "./item.tsx";
+import {IconAdd, IconDelete} from "@/components/icons";
+import ImageList from "@/components/article/list.tsx";
+import { BlockText} from "./item.tsx";
import styles from './article.module.scss'
-import {Button, Popconfirm} from "antd";
type Props = {
children?: React.ReactNode;
- index?:number;
+ index?: number;
className?: string;
blocks: BlockContent[];
editable?: boolean;
onChange?: (blocks: BlockContent[]) => void;
onRemove?: () => void;
onAdd?: () => void;
+ errorMessage?: string;
}
-export default function ArticleBlock({className, blocks, editable, onRemove, onAdd, onChange,index}: Props) {
+function rebuildBlockArray(blocks: BlockContent[]) {
+ const textBlock: BlockContent = {
+ type: 'text',
+ content: ''
+ }
+ const _blocks: BlockContent[] = [textBlock];
+ const textArray: string[] = []
+ blocks.forEach(it => {
+ if (it.type == 'text') {
+ textArray.push(it.content)
+ } else {
+ _blocks.push(it)
+ }
+ })
+ textBlock.content = textArray.join('\n')
+ return _blocks
+}
+
+
+export default function ArticleBlock(
+ {
+ className,
+ blocks: defaultBlocks,
+ editable,
+ onRemove,
+ onAdd,
+ onChange,
+ index,
+ errorMessage
+ }: Props) {
+ const blocks = rebuildBlockArray(defaultBlocks)
const handleBlockRemove = (index: number) => {
// 删除当前项
onChange?.(blocks.filter((_, idx) => index !== idx))
@@ -43,72 +75,39 @@ export default function ArticleBlock({className, blocks, editable, onRemove, onA
}
return
-
+
- {blocks.map((it, idx) => {
- const isFirstTextBlock = index == 0 && it.type ==='text' && firstTextBlockIndex == idx
- return (
-
- {
- it.type === 'text'
- ?
handleBlockChange(idx, block)} data={it}
- editable={editable}/>
- :
- }
- {editable &&
- {isFirstTextBlock?
:
- 请确认删除此{it.type === 'text' ? '文本' : '图片'}?
- }
- onConfirm={() => handleBlockRemove(idx)}
- okText="删除"
- cancelText="取消"
- >
-
-
-
- }
-
- handleAddBlock('text', idx + 1)}
- className="article-action-icon" title="新增文本">
- handleAddBlock('image', idx + 1)}
- className="article-action-icon mt-1" title="新增图片">
-
- }
-
- {isFirstTextBlock &&
该编辑框内容由数字人播报
}
-
)
- }
- )}
- {editable && blocks.length == 0 &&
-
-
-
-
-
-
- }
+
+
+ handleBlockChange(0, block)}
+ data={blocks[0]}
+ isFirstBlock={index == 0}
+ editable={editable}/>
+
+ {index == 0 &&
+
{errorMessage}
+
该编辑框内容由数字人播报
+
}
+
+ {index > 0 &&
}
{editable &&
}
- onConfirm={onRemove}
- okText="删除"
- cancelText="取消"
- >
-
-
+ {
+ index > 0 ?
请确认删除此删除此分组? }
+ onConfirm={onRemove}
+ okText="删除"
+ cancelText="取消"
+ >
+
+
+
+ :
+ }
}
diff --git a/src/components/article/edit-modal.tsx b/src/components/article/edit-modal.tsx
index 6a731b6..c658da7 100644
--- a/src/components/article/edit-modal.tsx
+++ b/src/components/article/edit-modal.tsx
@@ -2,11 +2,13 @@ import {Input, Modal} from "antd";
import ArticleGroup from "@/components/article/group.tsx";
import {useEffect, useState} from "react";
import {useSetState} from "ahooks";
-import {getById} from "@/service/api/article.ts";
+import * as article from "@/service/api/article.ts";
+import {regenerate} from "@/service/api/video.ts";
type Props = {
id?: number;
- onClose?: () => void;
+ type: 'news' | 'video';
+ onClose?: (saved?: boolean) => void;
}
export default function ArticleEditModal(props: Props) {
@@ -16,29 +18,59 @@ export default function ArticleEditModal(props: Props) {
const [state, setState] = useSetState({
loading: false,
- open: false
+ open: false,
+ msgTitle: '',
+ msgGroup: '',
})
+ // 保存数据
const handleSave = () => {
+ console.log(groups, title)
+ if (!title) {
+ // setState({msgTitle: '请输入标题内容'});
+ return;
+ }
+ if (groups.length == 0 || groups[0].length == 0 || !groups[0][0].content) {
+ // setState({msgGroup: '请输入正文文本内容'});
+ return;
+ }
+ const save = props.type == 'news' ? article.save : regenerate
+ setState({loading: true})
+ save(title, groups, props.id > 0 ? props.id : undefined).then(() => {
+ props.onClose?.(true)
+ }).finally(() => {
+ setState({loading: false})
+ });
props.onClose?.()
- // if (props.onSave) {
- // setState({loading: true})
- // props.onSave?.().then(() => {
- // setState({loading: false, open: false})
- // })
- // } else {
- // console.log(groups)
- // }
}
useEffect(() => {
- if(props.id){
- if(props.id > 0){
- getById(props.id).then(res => {
- setGroups(res.content_group)
- setTitle(res.title)
- })
- }else{
- setGroups([])
- setTitle('')
+ if (props.id) {
+ if (props.id > 0) {
+ if (props.type == 'news') {
+ article.getById(props.id).then(res => {
+ setGroups(res.content_group)
+ setTitle(res.title)
+ })
+ }
+ } else {
+ // 新增
+ setGroups([
+ [{
+ type: 'text',
+ content: '韩国国会当地时间14日16时举行全体会议,就在野党阵营第二次提出的尹锡悦总统弹劾案进行表决。根据投票结果,有204票赞成,85票反对,3票弃权,8票无效,弹劾案最终获得通过,尹锡悦的总统职务立即停止。'
+ }],
+ [
+ {
+ type: 'text',
+ content: '韩国宪法法院将在180天内完成弹劾案审判程序。如果宪法法院作出弹劾案不成立的裁决,尹锡悦将立即恢复总统职务;如果宪法法院认可弹劾案成立,尹锡悦将立即被罢免,预计韩国将在明年4月至6月间举行大选。'
+ },
+ {
+ type: 'image',
+ content: 'https://zverse-on.oss-cn-shanghai.aliyuncs.com/metahuman/workbench/20241214/193c442df75.jpeg'
+ },
+
+ ],
+ ])
+ setTitle('韩国国会通过总统弹劾案 尹锡悦职务立即停止')
}
}
}, [props.id])
@@ -49,7 +81,7 @@ export default function ArticleEditModal(props: Props) {
maskClosable={false}
keyboard={false}
width={800}
- onCancel={props.onClose}
+ onCancel={()=>props.onClose?.()}
okButtonProps={{loading: state.loading}}
onOk={handleSave}
>
@@ -59,18 +91,26 @@ export default function ArticleEditModal(props: Props) {
*
- {
+ {
setTitle(e.target.value)
+ setState({msgTitle: e.target.value ? '' : '请输入标题内容'})
}} placeholder={'请输入文章标题'}/>
+ {state.msgTitle}
-
+
正文
*
-
setGroups(() => list)}/>
+ {
+ setGroups(() => list)
+ setState({msgGroup: (list.length == 0 || list[0].length == 0 || !list[0][0].content) ? '请输入正文文本内容' : ''});
+ }}
+ />
);
diff --git a/src/components/article/group.tsx b/src/components/article/group.tsx
index 20963f8..cf2158d 100644
--- a/src/components/article/group.tsx
+++ b/src/components/article/group.tsx
@@ -1,22 +1,46 @@
-
import {message} from "antd"
import ArticleBlock from "@/components/article/block.tsx";
import styles from './article.module.scss'
+import {showToast} from "@/components/message.ts";
type Props = {
groups: BlockContent[][];
editable?: boolean;
onChange?: (groups: BlockContent[][]) => void;
+ errorMessage?: string;
}
-export default function ArticleGroup({groups, editable, onChange}: Props) {
+
+function rebuildGroups(groups: BlockContent[][]) {
+ if (groups.length < 2) {
+ Array(2 - groups.length).fill([{type: 'text', content: ''}]).forEach((it) => {
+ groups.push(it)
+ })
+ }
+
+ return groups;
+
+
+}
+
+export default function ArticleGroup({groups: _groups, editable, onChange,errorMessage}: Props) {
+ const groups = rebuildGroups(_groups)
/**
* 添加一个组
* @param insertIndex 插入的位置,-1表示插入到末尾
*/
- const handleAddGroup = ( insertIndex: number = -1) => {
- const newGroup: BlockContent[] = []
- const _groups = [...groups]
+ const handleAddGroup = (insertIndex: number = -1) => {
+ if (insertIndex !== -1 && insertIndex !== 1) {
+ const triggerGroup = insertIndex == -1 || insertIndex >= groups.length ? groups[groups.length - 1] : groups[insertIndex - 1];
+ // 判断
+ if (triggerGroup.length == 0 || triggerGroup.some(s => !s.content)) {
+ showToast('请先添加内容')
+ return;
+ }
+ }
+
+ const newGroup: BlockContent[] = [{type: 'text', content: ''}]
+ const _groups = [...groups];
if (insertIndex == -1 || insertIndex >= groups.length) { // -1或者越界表示新增
_groups.push(newGroup)
} else {
@@ -34,6 +58,7 @@ export default function ArticleGroup({groups, editable, onChange}: Props) {
groups[index] = blocks
onChange?.([...groups])
}}
+ errorMessage={errorMessage}
index={index}
onAdd={() => {
handleAddGroup?.(index + 1)
@@ -48,6 +73,7 @@ export default function ArticleGroup({groups, editable, onChange}: Props) {
/>
))}
{groups.length == 0 && editable &&
-
onChange?.([blocks])} index={0} blocks={[{type:'text',content:''}]}/>}
+ onChange?.([blocks])} index={0}
+ blocks={[{type: 'text', content: ''}]}/>}
}
\ No newline at end of file
diff --git a/src/components/article/item.tsx b/src/components/article/item.tsx
index 963cfbf..a1f367d 100644
--- a/src/components/article/item.tsx
+++ b/src/components/article/item.tsx
@@ -1,10 +1,12 @@
import React, {useState} from "react";
-import {Button, Input, Spin, Upload, UploadProps} from "antd";
+import {Input, Popconfirm, Spin, Upload, UploadProps} from "antd";
+import {CloseOutlined,CloudUploadOutlined} from "@ant-design/icons";
+import {clsx} from "clsx";
import styles from './article.module.scss'
import {getOssPolicy} from "@/service/api/common.ts";
import {showToast} from "@/components/message.ts";
-import {clsx} from "clsx";
+import {IconAddImage} from "@/components/icons";
type Props = {
children?: React.ReactNode;
@@ -14,11 +16,15 @@ type Props = {
onChange?: (data: BlockContent) => void;
isFirstBlock?: boolean;
}
+type ImageProps = {
+ onRemove?: () => void;
+ onlyUpload?: boolean;
+} & Props;
const MimeTypes = ['image/jpeg', 'image/png', 'image/jpg']
const Data: { uploadConfig?: TOSSPolicy } = {}
-export function BlockImage({data, editable, onChange}: Props) {
+export function BlockImage({data, editable, onChange, onlyUpload, onRemove}: ImageProps) {
const [loading, setLoading] = useState(-1)
// oss上传文件所需的数据
@@ -48,7 +54,7 @@ export function BlockImage({data, editable, onChange}: Props) {
console.log('onChange', file);
if (file.status == 'done') {
setLoading(-1)
- onChange?.({type: 'image', content: Data.uploadConfig?.host + file.url})
+ onChange?.({type: 'image', content: Data.uploadConfig?.host + '/' + file.url})
} else if (file.status == 'error') {
setLoading(-1)
showToast('上传图片失败,请重试', 'warning')
@@ -58,7 +64,15 @@ export function BlockImage({data, editable, onChange}: Props) {
}
//
return
- {editable ?
+ {editable ?
+ {!onlyUpload &&
请确认删除此删除此图片?}
+ onConfirm={onRemove}
+ okText="删除"
+ cancelText="取消"
+ >
+
+ }
= 0} percent={loading == 0 ? 'auto' : loading}>
更换图片
> :
}
@@ -84,13 +101,14 @@ export function BlockImage({data, editable, onChange}: Props) {
export function BlockText({data, editable, onChange, isFirstBlock}: Props) {
return
-
+
{editable ?
{
onChange?.({type: 'text', content: e.target.value})
}}
- placeholder={'请输入文本'} value={data.content} autoSize={{minRows: 3, maxRows: 8}}
+ placeholder={'请输入文本内容'} value={data.content} autoSize={{minRows: 3, maxRows: 8}}
variant={"borderless"}/>
:
{data.content}
}
diff --git a/src/components/article/list.tsx b/src/components/article/list.tsx
new file mode 100644
index 0000000..2a0bb6f
--- /dev/null
+++ b/src/components/article/list.tsx
@@ -0,0 +1,42 @@
+import React from "react";
+
+import {BlockImage} from "@/components/article/item.tsx";
+
+import styles from './article.module.scss'
+
+export default function ImageList(props: {
+ blocks: BlockContent[];
+ editable?: boolean;
+ onChange?: (blocks: BlockContent[]) => void;
+}) {
+
+ // 处理删除
+ const handleRemove = (index: number) => {
+ props.onChange?.(props.blocks.filter((_, idx) => index !== idx))
+ const newBlocks = [...props.blocks]
+ newBlocks.splice(index, 1)
+ props.onChange?.(newBlocks)
+ }
+ // 处理新增
+ const handleAdd = (data: BlockContent) => {
+ props.onChange?.([...props.blocks, data])
+ }
+ // 处理更新
+ const handleUpdate = (index: number, data: BlockContent) => {
+ props.onChange?.(props.blocks.map((it, idx) => idx === index ? data : it))
+ }
+
+
+ return (
+ {props.blocks.map((it, index) => (
+ it.type === 'image' ? handleUpdate(index, data)}
+ onRemove={() => handleRemove(index)}
+ /> : null
+ ))}
+ {props.editable &&
+ }
+
)
+}
+
diff --git a/src/pages/news/edit.tsx b/src/pages/news/edit.tsx
index 4baf032..924ff8b 100644
--- a/src/pages/news/edit.tsx
+++ b/src/pages/news/edit.tsx
@@ -14,12 +14,9 @@ export default function NewEdit() {
const [editId, setEditId] = useState(-1)
const [selectedRowKeys, setSelectedRowKeys] = useState
([])
const [params, setParams] = useState({
- pagination: {
- page: 1,
- limit: 10
- }
+ pagination: {page: 1, limit: 10}
})
- const {data} = useRequest(() => getList(params), {refreshDeps: [params]})
+ const {data, refresh} = useRequest(() => getList(params), {refreshDeps: [params]})
const columns: TableColumnsType = [
{
@@ -86,15 +83,20 @@ export default function NewEdit() {
showSizeChanger={false}
simple={true}
rootClassName={'simple-pagination'}
- onChange={(page) => setParams(prev=>({
+ onChange={(page) => setParams(prev => ({
...prev,
pagination: {page, limit: 10}
}))}
/>
-
+
}
- setEditId(-1)}/>
+ {
+ setEditId(-1)
+ if (saved) refresh()
+ }}/>
)
}
\ No newline at end of file
diff --git a/src/service/api/video.ts b/src/service/api/video.ts
index c9100c1..9d514ba 100644
--- a/src/service/api/video.ts
+++ b/src/service/api/video.ts
@@ -10,7 +10,7 @@ export function getList() {
* @param content_group
* @param article_id
*/
-export function regenerate(title: string, content_group: BlockContent[][], article_id: number) {
+export function regenerate(title: string, content_group: BlockContent[][], article_id?: Id) {
return post<{ content: string }>({
url: '/video/regenerate',
data: {