提交 17692dfa authored 作者: 王鹏飞's avatar 王鹏飞

chore: update

上级 f77be006
# VITE_API_URL_WORD = https://zijingebook.ezijing.com/file/ # VITE_API_URL_WORD = https://zijingebook.ezijing.com/file/
VITE_API_URL_WORD = https://book-admin-web.ezijing.com/api VITE_API_URL_WORD = https://zijingebook.ezijing.com/api
VITE_API_BASE_API_PREFFIX = /api VITE_API_BASE_API_PREFFIX = /api
# VITE_API_WEBSOCKET_URL = wss://zijingebook.ezijing.com # VITE_API_WEBSOCKET_URL = wss://zijingebook.ezijing.com
VITE_API_WEBSOCKET_URL = wss://book-admin-web.ezijing.com/ws VITE_API_WEBSOCKET_URL = wss://zijingebook.ezijing.com/ws
VITE_API_OPENAI_URL = https://model-platform-skyagents.tiangong.cn VITE_API_OPENAI_URL = https://model-platform-skyagents.tiangong.cn
\ No newline at end of file
# VITE_API_URL_WORD = https://zijingebook.ezijing.com/file/ # VITE_API_URL_WORD = http://ebook-pc.ezijing.com:7419
VITE_API_URL_WORD = https://book-admin-web.ezijing.com/api VITE_API_URL_WORD = https://zijingebook.ezijing.com/api
VITE_API_BASE_API_PREFFIX = /api VITE_API_BASE_API_PREFFIX = /api
# VITE_API_WEBSOCKET_URL = wss://zijingebook.ezijing.com # VITE_API_WEBSOCKET_URL = wss://zijingebook.ezijing.com
VITE_API_WEBSOCKET_URL = wss://book-admin-web.ezijing.com/ws VITE_API_WEBSOCKET_URL = wss://zijingebook.ezijing.com/ws
VITE_API_OPENAI_URL = https://model-platform-skyagents.tiangong.cn VITE_API_OPENAI_URL = https://model-platform-skyagents.tiangong.cn
\ No newline at end of file
差异被折叠。
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
"lint": "eslint --ext .js,.jsx,.ts,.tsx --fix --ignore-path .gitignore ./src" "lint": "eslint --ext .js,.jsx,.ts,.tsx --fix --ignore-path .gitignore ./src"
}, },
"dependencies": { "dependencies": {
"@ant-design/icons": "^5.3.7", "@ant-design/icons": "^5.4.0",
"@chuangkit/chuangkit-design": "^2.0.8", "@chuangkit/chuangkit-design": "^2.0.8",
"@fortaine/fetch-event-source": "^3.0.6", "@fortaine/fetch-event-source": "^3.0.6",
"@reduxjs/toolkit": "^1.9.7", "@reduxjs/toolkit": "^1.9.7",
...@@ -20,7 +20,7 @@ ...@@ -20,7 +20,7 @@
"@wangeditor/plugin-link-card": "^1.0.0", "@wangeditor/plugin-link-card": "^1.0.0",
"ahooks": "^3.8.0", "ahooks": "^3.8.0",
"ali-oss": "^6.20.0", "ali-oss": "^6.20.0",
"antd": "^5.18.1", "antd": "^5.20.6",
"axios": "^1.6.2", "axios": "^1.6.2",
"dayjs": "^1.11.11", "dayjs": "^1.11.11",
"easy-formula-editor": "^0.0.2-alpha.1", "easy-formula-editor": "^0.0.2-alpha.1",
...@@ -32,7 +32,7 @@ ...@@ -32,7 +32,7 @@
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"qs": "^6.11.2", "qs": "^6.11.2",
"rc-slider-captcha": "^1.3.0", "rc-slider-captcha": "^1.3.0",
"react": "^18.2.0", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-error-boundary": "^4.0.13", "react-error-boundary": "^4.0.13",
"react-redux": "^8.1.3", "react-redux": "^8.1.3",
...@@ -42,15 +42,15 @@ ...@@ -42,15 +42,15 @@
"xml-formatter": "^3.6.2" "xml-formatter": "^3.6.2"
}, },
"devDependencies": { "devDependencies": {
"@types/react": "^18.3.3", "@types/react": "^18.3.5",
"@types/react-dom": "^18.3.0", "@types/react-dom": "^18.3.0",
"@vitejs/plugin-react-swc": "^3.7.0", "@vitejs/plugin-react-swc": "^3.7.0",
"eslint": "^8.57.0", "eslint": "^8.57.0",
"eslint-plugin-react": "^7.34.2", "eslint-plugin-react": "^7.35.2",
"eslint-plugin-react-hooks": "^4.6.2", "eslint-plugin-react-hooks": "^4.6.2",
"less": "^4.2.0", "less": "^4.2.0",
"vite": "^5.3.1", "vite": "^5.4.4",
"vite-plugin-mkcert": "^1.17.5" "vite-plugin-mkcert": "^1.17.6"
}, },
"browserslist": [ "browserslist": [
"> 1%", "> 1%",
...@@ -58,6 +58,6 @@ ...@@ -58,6 +58,6 @@
"not ie <= 10" "not ie <= 10"
], ],
"optionalDependencies": { "optionalDependencies": {
"@rollup/rollup-linux-x64-gnu": "^4.18.0" "@rollup/rollup-linux-x64-gnu": "^4.21.2"
} }
} }
...@@ -52,9 +52,6 @@ dd { ...@@ -52,9 +52,6 @@ dd {
width: 25% !important; width: 25% !important;
min-width: 475px; min-width: 475px;
} }
.ant-modal .ant-modal-close {
inset-inline-end: initial;
}
.submit { .submit {
background: #aa1941; background: #aa1941;
...@@ -139,28 +136,28 @@ dd { ...@@ -139,28 +136,28 @@ dd {
// } // }
@font-face { @font-face {
font-family: "思源黑体"; font-family: '思源黑体';
src: url("https://zxts-book-file.zijingebook.com/2024/04/03/fonts-1712131326219-q886x58lgm.ttf") format('truetype'); src: url('https://zxts-book-file.zijingebook.com/2024/04/03/fonts-1712131326219-q886x58lgm.ttf') format('truetype');
} }
@font-face { @font-face {
font-family: "思源宋体"; font-family: '思源宋体';
src: url("https://zxts-book-file.zijingebook.com/2024/04/03/fonts-1712131348680-t3miidf3sxs.ttf") format('truetype'); src: url('https://zxts-book-file.zijingebook.com/2024/04/03/fonts-1712131348680-t3miidf3sxs.ttf') format('truetype');
} }
@font-face { @font-face {
font-family: "楷体"; font-family: '楷体';
src: url("https://zxts-book-file.zijingebook.com/2024/04/03/fonts-1712131272079-k30tgmq5m7.ttf") format('truetype'); src: url('https://zxts-book-file.zijingebook.com/2024/04/03/fonts-1712131272079-k30tgmq5m7.ttf') format('truetype');
} }
@font-face { @font-face {
font-family: "仿宋"; font-family: '仿宋';
src: url("https://zxts-book-file.zijingebook.com/2024/04/03/fonts-1712131169707-0x5pgjxiburq.ttf") format('truetype'); src: url('https://zxts-book-file.zijingebook.com/2024/04/03/fonts-1712131169707-0x5pgjxiburq.ttf') format('truetype');
} }
@font-face { @font-face {
font-family: "宋体"; font-family: '宋体';
src: url("https://zxts-book-file.zijingebook.com/2024/04/03/fonts-1712131298815-mogsisbs1vl.ttf") format('truetype'); src: url('https://zxts-book-file.zijingebook.com/2024/04/03/fonts-1712131298815-mogsisbs1vl.ttf') format('truetype');
} }
@font-face { @font-face {
font-family: "黑体"; font-family: '黑体';
src: url("https://zxts-book-file.zijingebook.com/2024/04/03/fonts-1712131247994-gp497hgawyb.ttf") format('truetype'); src: url('https://zxts-book-file.zijingebook.com/2024/04/03/fonts-1712131247994-gp497hgawyb.ttf') format('truetype');
} }
// 仿宋 http://zxts-book-file.zijingebook.com/2024/04/03/fonts-1712131169707-0x5pgjxiburq.ttf // 仿宋 http://zxts-book-file.zijingebook.com/2024/04/03/fonts-1712131169707-0x5pgjxiburq.ttf
...@@ -170,13 +167,14 @@ dd { ...@@ -170,13 +167,14 @@ dd {
// 思源黑体 http://zxts-book-file.zijingebook.com/2024/04/03/fonts-1712131326219-q886x58lgm.ttf // 思源黑体 http://zxts-book-file.zijingebook.com/2024/04/03/fonts-1712131326219-q886x58lgm.ttf
// 思源宋体 http://zxts-book-file.zijingebook.com/2024/04/03/fonts-1712131348680-t3miidf3sxs.ttf // 思源宋体 http://zxts-book-file.zijingebook.com/2024/04/03/fonts-1712131348680-t3miidf3sxs.ttf
.ant-table-wrapper .ant-table-container table>thead>tr:first-child >*:first-child { .ant-table-wrapper .ant-table-container table > thead > tr:first-child > *:first-child {
border-start-start-radius: 0; border-start-start-radius: 0;
} }
.ant-table-wrapper table, .ant-table-wrapper .ant-table .ant-table-header { .ant-table-wrapper table,
.ant-table-wrapper .ant-table .ant-table-header {
border-radius: 0; border-radius: 0;
} }
.ant-table-wrapper .ant-table-container table>thead>tr:first-child >*:last-child { .ant-table-wrapper .ant-table-container table > thead > tr:first-child > *:last-child {
border-start-end-radius: 0; border-start-end-radius: 0;
&.ant-table-cell-scrollbar { &.ant-table-cell-scrollbar {
background-color: #f5f5f5; background-color: #f5f5f5;
......
import axios from '@/utils/axios'
// 获取教师列表
export function getTeacherList(data) {
return axios.post('/api/user/teacher/getList', data)
}
import { useState, useEffect, useRef, forwardRef, useImperativeHandle } from 'react' import { useState, useEffect, useRef, forwardRef, useImperativeHandle } from 'react'
import { Button, Divider, Space, Modal, Drawer } from 'antd' import { Button, Divider, Space, Modal } from 'antd'
import { EyeOutlined, SaveOutlined, CloseOutlined, HistoryOutlined } from '@ant-design/icons' import { EyeOutlined, SaveOutlined, CloseOutlined, HistoryOutlined } from '@ant-design/icons'
import AliOSS from 'ali-oss' import AliOSS from 'ali-oss'
import dayjs from 'dayjs' import dayjs from 'dayjs'
...@@ -161,7 +161,6 @@ const WangEditorCustomer = (props, ref) => { ...@@ -161,7 +161,6 @@ const WangEditorCustomer = (props, ref) => {
const [content, setContent] = useState(html) const [content, setContent] = useState(html)
const saveRef = useRef() const saveRef = useRef()
const [selectText, setSelectText] = useState('') //
const [titleVisible, setTitleVisible] = useState(false) // 章头 const [titleVisible, setTitleVisible] = useState(false) // 章头
const [titleInfo, setTitleInfo] = useState({}) const [titleInfo, setTitleInfo] = useState({})
const [sectionVisible, setSectionVisible] = useState(false) const [sectionVisible, setSectionVisible] = useState(false)
...@@ -179,8 +178,6 @@ const WangEditorCustomer = (props, ref) => { ...@@ -179,8 +178,6 @@ const WangEditorCustomer = (props, ref) => {
const [expandVisible, setExpandVisible] = useState(false) // 扩展阅读 const [expandVisible, setExpandVisible] = useState(false) // 扩展阅读
const [expandInfo, setExpandInfo] = useState({}) // 扩展阅读内容 const [expandInfo, setExpandInfo] = useState({}) // 扩展阅读内容
const [aiVisible, setAIVisible] = useState(false) // ai对话弹窗
const [priviewVisible, setPreviewVisible] = useState(false) const [priviewVisible, setPreviewVisible] = useState(false)
const [historyVisible, setHistoryVisible] = useState(false) //点击历史 const [historyVisible, setHistoryVisible] = useState(false) //点击历史
...@@ -437,7 +434,7 @@ const WangEditorCustomer = (props, ref) => { ...@@ -437,7 +434,7 @@ const WangEditorCustomer = (props, ref) => {
}, 50) }, 50)
} }
editorConfig.onFocus = editor => { editorConfig.onFocus = () => {
clearTimeout(saveRef.current) clearTimeout(saveRef.current)
} }
editorConfig.onBlur = editor => { editorConfig.onBlur = editor => {
...@@ -536,9 +533,7 @@ const WangEditorCustomer = (props, ref) => { ...@@ -536,9 +533,7 @@ const WangEditorCustomer = (props, ref) => {
let info = {} let info = {}
for (let nodeEntry of nodeEntries) { for (let nodeEntry of nodeEntries) {
const [node, path] = nodeEntry const [node, path] = nodeEntry
// console.log('选中了 paragraph 节点', node) node.children.forEach(item => {
// console.log('节点 path 是', path)
node.children.forEach((item, index) => {
if (item.type === 'image') { if (item.type === 'image') {
info.image = item info.image = item
} }
...@@ -572,12 +567,6 @@ const WangEditorCustomer = (props, ref) => { ...@@ -572,12 +567,6 @@ const WangEditorCustomer = (props, ref) => {
setExpandVisible(true) setExpandVisible(true)
}) })
// ai对话
editor.on('AISelectTextClick', () => {
setSelectText(editor.getSelectionText())
setAIVisible(true)
})
editor.addMark('fontSize', '18px') editor.addMark('fontSize', '18px')
editor.addMark('fontFamily', '黑体') editor.addMark('fontFamily', '黑体')
editor.addMark('lineHeight', 1.5) editor.addMark('lineHeight', 1.5)
......
import { useRef, useState } from 'react';
const useWebsocket = ({}) => {
const ws = useRef();
const [wsMessage, setWsMessage] = useState({});
const [wsReadyState, setWsReadyState] = useState({ key: 0, value: '正在连接中' });
const createWebSocket = (url) => {
const stateArr = [
{ key: 0, value: '正在连接中' },
{ key: 1, value: '已经连接并且可以通讯' },
{ key: 2, value: '连接正在关闭' },
{ key: 3, value: '连接已关闭或者没有连接成功' },
];
try {
ws.current = new WebSocket(url);
ws.current.onopen = () => {
console.log('websocket open');
setWsReadyState(stateArr[ws.current?.readyState ?? 0]);
};
ws.current.onclose = () => {
console.log('websocket close');
setWsReadyState(stateArr[ws.current?.readyState ?? 0]);
};
ws.current.onerror = () => {
console.log('websocket error');
setWsReadyState(stateArr[ws.current?.readyState ?? 0]);
};
ws.current.onmessage = (e) => {
console.log('onmessage', e);
setWsMessage(JSON.parse(e.data));
};
} catch (error) {
console.log(error);
}
};
const webSocketInit = (url) => {
if (!ws.current || ws.current.readyState === 3) {
createWebSocket(url);
}
};
// 关闭 WebSocket
const closeWebSocket = () => {
ws.current?.close();
};
// 发送数据
const sendMessage = (str) => {
ws.current?.send(str);
};
//重连
const reconnect = () => {
try {
closeWebSocket();
ws.current = null;
createWebSocket();
} catch (e) {
console.log(e);
}
};
// useEffect(() => {
// webSocketInit();
// return () => {
// ws.current?.close(1000);
// };
// }, [ws]);
return {
webSocketInit,
wsMessage,
setWsMessage,
wsReadyState,
reconnect,
sendMessage,
closeWebSocket,
};
};
export default useWebsocket;
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
import { Spin } from 'antd'
import { Editor, Toolbar } from '@wangeditor/editor-for-react' import { Editor, Toolbar } from '@wangeditor/editor-for-react'
import '@wangeditor/editor/dist/css/style.css' // 引入 css import '@wangeditor/editor/dist/css/style.css' // 引入 css
import './styles.less' import './styles.less'
import { fontFamilyList, fontSizeList, lineHeightList } from '@/common/wangeditor-customer/utils/setting'
import { uploadFile } from '@/utils/oss'
function WangEditorCustomer() { export default function WangEditor({ value, onChange }) {
// editor 实例 // editor 实例
const [editor, setEditor] = useState(null) // JS 语法 const [editor, setEditor] = useState(null) // JS 语法
// 编辑器内容 const [loading, setLoading] = useState(false)
const [html, setHtml] = useState('<p>hello</p>')
// 模拟 ajax 请求,异步设置 html
useEffect(() => {
setTimeout(() => {
setHtml('<p>hello world</p>')
}, 1500)
}, [])
// 工具栏配置 // 工具栏配置
const toolbarConfig = { const toolbarConfig = {
toolbarKeys: [ excludeKeys: ['insertVideo', 'insertImage', 'emotion', 'table', 'codeBlock', 'blockquote', 'code', 'group-more-style', 'insertTable'] //删除工具栏
'redo',
'undo',
'|',
'fontFamily',
'fontSize',
'lineHeight',
'color',
'bold',
'italic',
'through',
'underline',
'bgColor',
'bulletedList',
'numberedList',
'indent',
'delIndent',
'sub',
'sup',
'justifyLeft',
'justifyCenter',
'justifyRight',
'justifyJustify',
'divider',
'|',
'insertTable',
'|',
'codeBlock', // 代码块
'blockquote' // 引用
]
} }
// 编辑器配置 // 编辑器配置
const editorConfig = { const editorConfig = {
placeholder: '请输入内容...' // JS 语法
placeholder: '请输入内容...',
MENU_CONF: {
// 配置默认字号
fontSize: { fontSizeList },
fontFamily: { fontFamilyList },
lineHeight: { lineHeightList },
// 配置上传图片
uploadImage: {
timeout: 5 * 1000, // 5s
fieldName: 'image',
headers: {
'Content-Type': 'multipart/form-data',
Authorization: 'Bearer ' + localStorage.getItem('token') || ''
},
maxFileSize: 10 * 1024 * 1024, // 10M
base64LimitSize: 5 * 1024, // 5kb 以下插入 base64 // 用户自定义上传图片
customUpload: async (file, insertFn) => {
setLoading(true)
const url = await uploadFile(file)
insertFn(url, '图片')
setLoading(false)
}
},
uploadVideo: {
timeout: 15 * 1000, // 5s
fieldName: 'video',
headers: {
'Content-Type': 'multipart/form-data',
Authorization: 'Bearer ' + localStorage.getItem('token') || ''
},
allowedFileTypes: ['video/mp4', 'video/ogg', 'video/webm'],
maxFileSize: 10 * 1024 * 1024, // 10M
base64LimitSize: 5 * 1024, // 5kb 以下插入 base64 // 用户自定义上传图片
customUpload: async (file, insertFn) => {
setLoading(true)
const url = await uploadFile(file)
insertFn(url, '视频')
setLoading(false)
}
}
},
hoverbarKeys: {
image: {
menuKeys: ['imageWidth30', 'imageWidth50', 'imageWidth100', 'deleteImage']
},
text: {
menuKeys: ['headerSelect', 'insertLink', '|', 'bold', 'underline', 'italic', '|', 'color', 'bgColor', 'clearStyle']
}
}
} }
useEffect(() => { useEffect(() => {
return () => { return () => {
if (editor === null) return if (editor === null) return
...@@ -64,52 +78,14 @@ function WangEditorCustomer() { ...@@ -64,52 +78,14 @@ function WangEditorCustomer() {
} }
}, [editor]) }, [editor])
const onCreated = editor => {
setEditor(editor)
setTimeout(() => {
const editorToolbar = document.querySelector('.editor-toolbar')
// 设置菜单模块标题
const dividerElements = editorToolbar.querySelectorAll('.w-e-bar-divider')
const dividerTitles = ['常用格式', '媒体资源', '高级模块', 'AI辅助', 'AI数字人']
dividerElements.forEach((element, index) => {
element.innerHTML = dividerTitles[index]
})
// 设置菜单标题
const menuButtonElements = editorToolbar.querySelectorAll('.w-e-bar-item button')
menuButtonElements.forEach((element, index) => {
if (index > 1 && index < 22) return
element.classList.add('has-title')
const title = element.getAttribute('data-tooltip')
const span = document.createElement('span')
span.innerHTML = title
span.className = 'title'
element.appendChild(span)
})
}, 50)
}
return ( return (
<> <>
<div className="editor-wrapper"> <div className="editor-wrapper">
<div className="editor-left"> <Spin spinning={loading}>
<div className="editor-header"></div>
<div className="editor-main">
<Editor
defaultConfig={editorConfig}
value={html}
onCreated={onCreated}
onChange={editor => setHtml(editor.getHtml())}
mode="default"
style={{ height: '500px', overflowY: 'hidden' }}
/>
</div>
</div>
<div className="editor-right">
<Toolbar editor={editor} defaultConfig={toolbarConfig} mode="default" className="editor-toolbar" /> <Toolbar editor={editor} defaultConfig={toolbarConfig} mode="default" className="editor-toolbar" />
</div> <Editor defaultConfig={editorConfig} value={value} onChange={onChange} onCreated={setEditor} mode="default" className="editor-main" />
</Spin>
</div> </div>
</> </>
) )
} }
export default WangEditorCustomer
.editor-wrapper { .editor-wrapper {
display: flex; border: 1px solid #d9d9d9;
}
.editor-left {
flex: 1;
border: 1px solid #ccc;
}
.editor-right {
flex: 0 0 300px;
background: #fafafa;
border: 1px solid #e5e5e5;
min-width: 300px;
margin-left: 10px;
border-radius: 6px; border-radius: 6px;
} overflow: hidden;
.editor-toolbar {
.editor-toolbar { border-bottom: 1px solid #d9d9d9;
margin: 10px;
.w-e-bar {
padding: 0;
svg {
width: 18px;
height: 18px;
}
}
.w-e-bar-divider {
margin: 0;
padding-top: 10px;
width: 100%;
height: auto;
background: #fafafa;
font-size: 14px;
font-weight: 600;
line-height: 40px;
}
.w-e-bar-item {
height: auto;
button {
svg:nth-child(2) {
display: none;
}
&.has-title {
width: 100%;
height: auto;
padding: 10px;
flex-direction: column;
align-items: center;
.title {
margin: 5px 0 0 0;
}
}
} }
.editor-main {
height: 400px;
overflow-y: hidden;
} }
} }
import { useMemo, useRef, useState } from 'react'
import { Select, Spin } from 'antd'
import { debounce } from 'lodash-es'
import { getTeacherList } from '@/api/user'
function DebounceSelect({ fetchOptions, debounceTimeout = 800, ...props }) {
const [fetching, setFetching] = useState(false)
const [options, setOptions] = useState([])
const fetchRef = useRef(0)
const debounceFetcher = useMemo(() => {
const loadOptions = value => {
fetchRef.current += 1
const fetchId = fetchRef.current
setOptions([])
setFetching(true)
fetchOptions(value).then(newOptions => {
if (fetchId !== fetchRef.current) {
// for fetch callback order
return
}
setOptions(newOptions)
setFetching(false)
})
}
return debounce(loadOptions, debounceTimeout)
}, [fetchOptions, debounceTimeout])
return (
<Select
labelInValue
filterOption={false}
onSearch={debounceFetcher}
notFoundContent={fetching ? <Spin size="small" /> : null}
{...props}
options={options}
/>
)
}
// Usage of DebounceSelect
async function fetchUserList(username) {
console.log('fetching user', username)
return getTeacherList({ real_name: username, page_size: 40 }).then(res => {
return res.data.list
})
}
const SelectTeacher = props => {
const [value, setValue] = useState([])
return (
<DebounceSelect
mode="multiple"
value={value}
placeholder="Select users"
fetchOptions={fetchUserList}
fieldNames={{ label: 'real_name', value: 'id' }}
onChange={newValue => {
setValue(newValue)
}}
style={{ width: '100%' }}
{...props}
/>
)
}
export default SelectTeacher
import { Button, Upload, message } from 'antd'
import { useState } from 'react'
import { uploadFile } from '@/utils/oss'
import './index.less'
export default function UploadImage({ value, onChange }) {
const [uploading, setUploading] = useState(false)
const uploadProps = {
name: 'file',
accept: '.jpg,.png',
showUploadList: false,
beforeUpload: async file => {
const suffixArr = ['jpg', 'png']
const suffix = file.name.substring(file.name.lastIndexOf('.') + 1)
if (!suffixArr.includes(suffix)) {
message.error(`请上传以${suffixArr.join('、')}格式的书籍`)
return false
} else if (file.size >= 1024 * 1024) {
message.error(`请上传大小在1M以内的图片`)
return false
}
},
customRequest: async ({ file }) => {
try {
setUploading(true)
const url = await uploadFile(file)
setUploading(false)
onChange(url)
return true
} catch (error) {
message.error('上传文件失败,请重试')
setUploading(false)
return false
}
}
}
return (
<div className="upload-image">
{value && <img src={value} width={88} height={88} style={{ borderRadius: 5 }} />}
<div className="upload-image-main">
<Upload {...uploadProps}>
<Button type="primary" ghost loading={uploading}>
点击上传
</Button>
</Upload>
<span className="tips">尺寸:只能上传jpg/png文件,且不超过1M</span>
</div>
</div>
)
}
.upload-image {
.upload-image-main {
display: flex;
align-items: center;
gap: 10px;
.tips {
font-size: 10px;
font-family: 'PingFangSC, PingFang SC';
font-weight: 400;
color: #999999;
line-height: 1;
font-size: 12px;
}
}
}
import { useState, useEffect } from 'react'
import { getTeacherList } from '@/api/user'
export function useTeachers() {
const [teachers, setTeachers] = useState([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
useEffect(() => {
const fetchTeachers = async () => {
try {
const response = await getTeacherList({ page_size: 40 })
setTeachers(response.data.list)
} catch (err) {
setError(err)
} finally {
setLoading(false)
}
}
fetchTeachers()
}, [])
return { teachers, loading, error }
}
.ant-modal .ant-modal-close {
inset-inline-end: initial;
}
.delModal, .delModal,
.footer { .footer {
.ant-btn-primary { .ant-btn-primary {
......
.upload {
background: rgba(170, 25, 65, 0.03);
border-radius: 4px;
border: 1px solid #aa1941;
color: #aa1941;
font-size: 12px;
&:hover {
background: rgba(170, 25, 65, 0.03) !important;
border-radius: 4px !important;
opacity: 0.7;
border: 1px solid #aa1941 !important;
color: #aa1941 !important;
font-size: 12px !important;
}
}
.uploadPar {
.descBox {
display: flex;
align-items: flex-end; /* 让子元素底部对齐 */
.desc {
font-size: 10px;
font-family: 'PingFangSC, PingFang SC';
font-weight: 400;
color: #999999;
line-height: 14px;
font-size: 12px;
margin-left: 6px;
margin-bottom: 2px;
}
}
}
.back-icon {
position: absolute;
top: 10px;
left: 10px;
cursor: pointer;
display: none;
}
.editor-box {
.w-e-scroll {
font-family: "黑体";
font-size: 18px;
line-height: 1.5;
}
}
\ No newline at end of file
import axios from '@/utils/axios'
// 获取教师书籍列表
export function getBookList(data) {
return axios.post('/api/book/teacher/getList', data)
}
// 获取教师书籍详情
export function getBook(data) {
return axios.post('/api/book/teacher/getInfoById', data)
}
// 添加教师书籍
export function addBook(data) {
return axios.post('/api/book/teacher/add', data)
}
// 添加教师书籍
export function updateBook(data) {
return axios.post('/api/book/teacher/edit', data)
}
// 删除教师书籍
export function deleteBook(data) {
return axios.post('/api/book/teacher/del', data)
}
import axios from '@/utils/axios'
// 获取章节编辑者信息
export function getChapterEditors(data) {
return axios.post('/api/book/teacher/chapter/getEditors', data)
}
// 保存章节编辑者
export function updateChapterEditors(data) {
return axios.post('/api/book/teacher/chapter/saveEditors', data)
}
import { useState, useEffect } from 'react'
import { Modal, Form, Radio, Space, message } from 'antd'
import { getChapterEditors, updateChapterEditors } from '../api'
const EditChapterEditors = ({ chapter = {}, onChange, ...props }) => {
const [editors, setEditors] = useState([])
const [value, setValue] = useState('')
useEffect(() => {
getChapterEditors({ book_id: chapter.book_id, chapter_id: chapter.id }).then(res => {
setEditors(res.data.editors || [])
const value = res.data.selected_editor_ids[0] || ''
setValue(value)
})
}, [chapter])
const handleSubmit = async () => {
await updateChapterEditors({ book_id: chapter.book_id, chapter_id: chapter.id, editor_id: value })
message.success('设置成功')
onChange && onChange()
}
return (
<Modal title="设置章节编写者" {...props} onOk={handleSubmit}>
<Form labelCol={{ span: 8 }}>
<Form.Item label="当前章节">{chapter.title}</Form.Item>
<Form.Item label="编写者">
<Radio.Group value={value} onChange={e => setValue(e.target.value)}>
<Space direction="vertical">
{editors.map(item => {
return (
<Radio value={item.editor_id} key={item.editor_id}>
{item.real_name}
</Radio>
)
})}
</Space>
</Radio.Group>
</Form.Item>
</Form>
</Modal>
)
}
export default EditChapterEditors
import React, { useState, useEffect, useRef } from 'react'; import { useState, useEffect, useRef } from 'react'
import { Form, Button, Space, Input, message } from 'antd'; import { Form, Button, Space, Input } from 'antd'
import { setTreeChapter } from '@/store/modules/user'; import { setTreeChapter } from '@/store/modules/user'
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch } from 'react-redux'
import { chapterEdit, chapterAdd, sectionAdd } from '../request'; import { chapterEdit, chapterAdd, sectionAdd } from '../request'
const EditChapterTitle = (props) => { const EditChapterTitle = props => {
const { const { editValue, setEditValue, editKey, setEditKey, parentId, bookId, getChapterTreeList, expandedKeys, setExpandedKeys, editorLoading, newChapterChange } =
editValue, props
setEditValue, const dispatch = useDispatch()
editKey, const [form] = Form.useForm()
setEditKey, const [loading, setLoading] = useState(false)
parentId, const [initValues, setInitValues] = useState({ title: editValue })
bookId, const inputRef = useRef()
getChapterTreeList,
expandedKeys,
setExpandedKeys,
editorLoading,
newChapterChange,
} = props;
const dispatch = useDispatch();
const [form] = Form.useForm();
const [loading, setLoading] = useState(false);
const [initValues, setInitValues] = useState({ title: editValue });
const inputRef = useRef();
useEffect(() => { useEffect(() => {
if (inputRef.current) { if (inputRef.current) {
inputRef.current.focus(); inputRef.current.focus()
} }
}, []); }, [])
useEffect(() => { useEffect(() => {
if (inputRef.current) { if (inputRef.current) {
inputRef.current.focus(); inputRef.current.focus()
} }
}, [editorLoading]); }, [editorLoading])
useEffect(() => { useEffect(() => {
form.setFieldsValue({ title: editValue }); form.setFieldsValue({ title: editValue })
setInitValues({ title: editValue }); setInitValues({ title: editValue })
}, [editValue]); }, [editValue])
const onFinish = async (values) => { const onFinish = async values => {
setLoading(true); setLoading(true)
let data = null; let data = null
if (parentId) { if (parentId) {
data = await chapterAdd({ data = await chapterAdd({
pid: parentId === -1 ? 0 : parentId, pid: parentId === -1 ? 0 : parentId,
book_id: bookId, book_id: bookId,
name: values.title, name: values.title
}); })
} else { } else {
data = await chapterEdit({ id: editKey, name: values.title }); data = await chapterEdit({ id: editKey, name: values.title })
} }
await dispatch(setTreeChapter({ saveBookId: bookId, saveChapterId: data.id })); await dispatch(setTreeChapter({ saveBookId: bookId, saveChapterId: data.id }))
if (data) { if (data) {
if (parentId) { if (parentId) {
await sectionAdd({ book_id: bookId, chapter_id: data.id, content: '', word_count: 0 }); await sectionAdd({ book_id: bookId, chapter_id: data.id, content: '', word_count: 0 })
} }
setEditKey(false); setEditKey(false)
setEditValue(null); setEditValue(null)
await getChapterTreeList(); await getChapterTreeList()
if (parentId) { if (parentId) {
let temp = [...expandedKeys, parentId]; let temp = [...expandedKeys, parentId]
setExpandedKeys(temp); setExpandedKeys(temp)
await newChapterChange(data.id, values.title); await newChapterChange(data.id, values.title)
}
} }
setLoading(false)
} }
setLoading(false);
};
return ( return (
<Form layout='vertical' form={form} onFinish={onFinish} initialValues={initValues}> <Form layout="vertical" form={form} onFinish={onFinish} initialValues={initValues}>
<Form.Item <Form.Item label="章节标题" name="title" rules={[{ required: true, message: '请输入章节标题' }]} extra="最多30个字符">
label='章节标题' <Input ref={inputRef} autoFocus maxLength={30} placeholder="" allowClear />
name='title'
rules={[{ required: true, message: '请输入章节标题' }]}
extra='最多30个字符'
>
<Input ref={inputRef} autoFocus maxLength={30} placeholder='' allowClear />
</Form.Item> </Form.Item>
<Form.Item className='editor-form-buttons' wrapperCol={{ offset: 9, span: 16 }}> <Form.Item className="editor-form-buttons" wrapperCol={{ offset: 9, span: 16 }}>
<Space size={20}> <Space size={20}>
<Button type='default' disabled={loading} onClick={() => setEditKey(false)}> <Button type="default" disabled={loading} onClick={() => setEditKey(false)}>
取消 取消
</Button> </Button>
<Button type='primary' loading={loading} htmlType='submit'> <Button type="primary" loading={loading} htmlType="submit">
确认 确认
</Button> </Button>
</Space> </Space>
</Form.Item> </Form.Item>
</Form> </Form>
); )
}; }
export default EditChapterTitle; export default EditChapterTitle
import React, { useState, useEffect, useRef } from 'react' import { useState, useEffect, useRef } from 'react'
import { Divider, Button, Row, Col, Descriptions, Tree, Tooltip, Dropdown, Space, Input, Popconfirm, Modal, Spin } from 'antd' import { Divider, Button, Row, Col, Descriptions, Tree, Dropdown, Space, Modal, Spin } from 'antd'
import { import { EllipsisOutlined, DiffOutlined, MenuFoldOutlined, MenuUnfoldOutlined } from '@ant-design/icons'
DiffOutlined,
MenuFoldOutlined,
MenuUnfoldOutlined,
EyeOutlined,
DashOutlined,
CloseOutlined,
CheckOutlined,
EllipsisOutlined
} from '@ant-design/icons'
import WangEditorCustomer from '@/common/wangeditor-customer' import WangEditorCustomer from '@/common/wangeditor-customer'
import { useLocation, useNavigate } from 'react-router-dom' import { useLocation, useNavigate } from 'react-router-dom'
import { useDispatch, useSelector } from 'react-redux' import { useDispatch, useSelector } from 'react-redux'
import { setAutosaveTime } from '@/store/modules/editor' import { setAutosaveTime } from '@/store/modules/editor'
import { setTreeChapter } from '@/store/modules/user' import { setTreeChapter } from '@/store/modules/user'
import EditChapterTitle from './components/form-chapter-title' import EditChapterTitle from './components/form-chapter-title'
import EditChapterEditors from './components/EditChapterEditors'
import { get } from 'lodash-es' import { get } from 'lodash-es'
import md5 from 'js-md5' import md5 from 'js-md5'
...@@ -34,7 +26,6 @@ const Examine = () => { ...@@ -34,7 +26,6 @@ const Examine = () => {
const [gData, setGData] = useState([]) const [gData, setGData] = useState([])
const [chapterId, setChapterId] = useState(0) const [chapterId, setChapterId] = useState(0)
const [bookId, setBookId] = useState(0) const [bookId, setBookId] = useState(0)
const [nameList, setNameList] = useState([])
const [openDel, setOpenDel] = useState(false) const [openDel, setOpenDel] = useState(false)
const [delNode, setDelNode] = useState({}) const [delNode, setDelNode] = useState({})
const [recordList, setRecordList] = useState([]) const [recordList, setRecordList] = useState([])
...@@ -53,6 +44,12 @@ const Examine = () => { ...@@ -53,6 +44,12 @@ const Examine = () => {
const [contentMd5, setContentMd5] = useState('') const [contentMd5, setContentMd5] = useState('')
const [quanXian, setQuanXian] = useState(false) const [quanXian, setQuanXian] = useState(false)
// 设置编者
const [currentChapter, setCurrentChapter] = useState(null)
const [editChapterEditorsIsOpen, setEditChapterEditorsIsOpen] = useState(false)
const onChapterEditorsChange = () => {
setEditChapterEditorsIsOpen(false)
}
// 编辑器内容 // 编辑器内容
const editorRef = useRef() const editorRef = useRef()
const saveInterRef = useRef() const saveInterRef = useRef()
...@@ -262,36 +259,6 @@ const Examine = () => { ...@@ -262,36 +259,6 @@ const Examine = () => {
setEditValue('') setEditValue('')
setParentId(-1) setParentId(-1)
} }
const dropDownMenuHandler = async (e, node) => {
e.domEvent.stopPropagation()
e.domEvent.preventDefault()
if (parseInt(e.key) === 1) {
// 展开
const ids = findNodeById(gData, 'key', node.key)
const expandedKeysTemp = [...expandedKeys, node.key]
ids.forEach((item, index) => {
if (!expandedKeysTemp.includes(item)) {
expandedKeysTemp.push(item)
}
})
setExpandedKeys(expandedKeysTemp)
} else if (parseInt(e.key) === 2) {
// 插入子节
// 编辑
setEditKey(-1)
setEditValue('')
setParentId(node.key)
} else if (parseInt(e.key) === 3) {
// 编辑
setEditKey(node.key)
setEditValue(node.title)
setParentId(false)
} else if (parseInt(e.key) === 4) {
setDelNode(node)
setOpenDel(true)
}
}
const delChapter = async node => { const delChapter = async node => {
setDelLoading(true) setDelLoading(true)
...@@ -350,90 +317,48 @@ const Examine = () => { ...@@ -350,90 +317,48 @@ const Examine = () => {
} }
}, [gData, treeChapter]) }, [gData, treeChapter])
useEffect(() => { const chapterMenuItems = [
console.log(expandedKeys) { key: '1', label: '展开全部' },
}, [expandedKeys]) { key: '2', label: '添加子节' },
{ key: '5', label: '设置编者' },
{ key: '3', label: '编辑' },
{ key: '4', label: '删除' }
]
// 编辑章节名称 const handleMenuClick = async (e, node) => {
const titleRenderDom = node => { e.domEvent.stopPropagation()
return ( setCurrentChapter(node)
<div className="tree-customer-item"> if (parseInt(e.key) === 1) {
<div className="title"> // 展开全部
<div className="title-node">{node.title}</div> const ids = findNodeById([node], 'key', node.key)
</div> const expandedKeysTemp = [...expandedKeys, node.key, ...ids]
<div className="opaeration"> setExpandedKeys(expandedKeysTemp)
{quanXian ? ( } else if (parseInt(e.key) === 2) {
<Dropdown // 添加子节
trigger={['click']} setEditKey(-1)
overlayClassName="dropmenu_list" setEditValue('')
destroyPopupOnHide={true} setParentId(node.key)
menu={{ } else if (parseInt(e.key) === 3) {
items: [ // 编辑
{ setEditKey(node.key)
key: '1', setEditValue(node.title)
label: <Button type="text">展开全部</Button> setParentId(false)
}, } else if (parseInt(e.key) === 4) {
{ setDelNode(node)
key: '2', setOpenDel(true)
label: <Button type="text">添加子节</Button> } else if (e.key == 5) {
}, setEditChapterEditorsIsOpen(true)
{
key: '5',
label: <Button type="text">设置编者</Button>
},
{
key: '3',
label: <Button type="text">重命名</Button>
},
{
key: '4',
label: <Button type="text">删除</Button>
} }
],
onClick: e => dropDownMenuHandler(e, node)
}}>
<div className="dashed">
<span></span>
<span></span>
<span></span>
</div>
{/* <EllipsisOutlined /> */}
</Dropdown>
) : (
<Dropdown
trigger={['click']}
overlayClassName="dropmenu_list"
destroyPopupOnHide={true}
menu={{
items: [
{
key: '1',
label: <Button type="text">展开全部</Button>
},
{
key: '2',
label: <Button type="text">添加子节</Button>
},
{
key: '3',
label: <Button type="text">编辑</Button>
},
{
key: '4',
label: <Button type="text">删除</Button>
} }
],
onClick: e => dropDownMenuHandler(e, node) // 章节名称
}}> const chapterTitleRender = node => {
<div className="dashed"> return (
<span></span> <div className="chapter-tree-item">
<span></span> <p className="title">{node.title}</p>
<span></span> <Dropdown trigger={['click']} menu={{ items: chapterMenuItems, onClick: e => handleMenuClick(e, node) }}>
</div> <EllipsisOutlined onClick={e => e.stopPropagation()} />
{/* <EllipsisOutlined /> */}
</Dropdown> </Dropdown>
)}
</div>
</div> </div>
) )
} }
...@@ -534,7 +459,7 @@ const Examine = () => { ...@@ -534,7 +459,7 @@ const Examine = () => {
disabled={loading} disabled={loading}
treeData={gData} treeData={gData}
onExpand={onExpand} onExpand={onExpand}
titleRender={nodeData => titleRenderDom(nodeData)} titleRender={nodeData => chapterTitleRender(nodeData)}
/> />
)} )}
</> </>
...@@ -623,8 +548,8 @@ const Examine = () => { ...@@ -623,8 +548,8 @@ const Examine = () => {
wrapper: 'chapter-title-modal' wrapper: 'chapter-title-modal'
}}> }}>
<Divider /> <Divider />
<div className="">确认删除子节【{delNode.title}】?</div> <div>确认删除子节【{delNode.title}】?</div>
<div className="" style={{ display: 'flex', justifyContent: 'flex-end' }}> <div style={{ display: 'flex', justifyContent: 'flex-end' }}>
<Space> <Space>
<Button type="primary" danger loading={delLoading} onClick={() => delChapter(delNode)}> <Button type="primary" danger loading={delLoading} onClick={() => delChapter(delNode)}>
确定 确定
...@@ -640,6 +565,15 @@ const Examine = () => { ...@@ -640,6 +565,15 @@ const Examine = () => {
</Space> </Space>
</div> </div>
</Modal> </Modal>
{/* 设置章节编写者 */}
{currentChapter && editChapterEditorsIsOpen && (
<EditChapterEditors
open={editChapterEditorsIsOpen}
chapter={currentChapter}
onCancel={() => setEditChapterEditorsIsOpen(false)}
onChange={onChapterEditorsChange}></EditChapterEditors>
)}
</div> </div>
) )
} }
......
...@@ -30,7 +30,7 @@ ...@@ -30,7 +30,7 @@
.section-left-top { .section-left-top {
height: 85px; height: 85px;
} }
.ant-descriptions.ant-descriptions-bordered >.ant-descriptions-view .ant-descriptions-row { .ant-descriptions.ant-descriptions-bordered > .ant-descriptions-view .ant-descriptions-row {
border-bottom: 1px solid #e5e5e5; border-bottom: 1px solid #e5e5e5;
&:nth-of-type(2) { &:nth-of-type(2) {
display: none; display: none;
...@@ -39,13 +39,14 @@ ...@@ -39,13 +39,14 @@
} }
.editor-right { .editor-right {
height: 100%; height: 100%;
.ant-spin-nested-loading, .ant-spin-container { .ant-spin-nested-loading,
.ant-spin-container {
height: 100%; height: 100%;
} }
} }
} }
.draggable-tree { .draggable-tree {
flex:1; flex: 1;
height: calc(100vh - 285px); height: calc(100vh - 285px);
overflow-y: auto; overflow-y: auto;
overflow-x: hidden; overflow-x: hidden;
...@@ -61,11 +62,10 @@ ...@@ -61,11 +62,10 @@
overflow: hidden; overflow: hidden;
} }
} }
.tree-customer-item { .chapter-tree-item {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
overflow: hidden; overflow: hidden;
width: 100%;
.title { .title {
flex: 1; flex: 1;
overflow: hidden; overflow: hidden;
...@@ -74,43 +74,7 @@ ...@@ -74,43 +74,7 @@
max-height: 24px; max-height: 24px;
line-height: 24px; line-height: 24px;
white-space: nowrap; white-space: nowrap;
.title-node {
overflow: hidden;
text-overflow: ellipsis;
padding-right: 10px;
max-height: 24px;
line-height: 24px;
}
}
.opaeration {
flex: 0 0 20px;
width: 20px;
align-items: center;
.dashed {
height: 24px;
line-height: 24px;
text-align: center;
align-items: center;
display: flex;
span {
margin: 0 1px;
display: inline-block;
width: 2px;
height: 2px;
// border: 1px solid #999;
background-color: #999;
border-radius: 50%;
}
}
}
}
}
.dropmenu_list {
.ant-dropdown .ant-dropdown-menu .ant-dropdown-menu-item:hover {
} }
.ant-btn-text:not(:disabled):not(.ant-btn-disabled):hover {
background: transparent !important;
} }
} }
......
import { useEffect, useState, useRef } from 'react'; import { useEffect, useState, useRef } from 'react'
import { Input, Button, Space, Popover, Switch, Image, App } from 'antd'; import { Input, Button, Space, Popover, Switch, Image, App } from 'antd'
import { getCoupon, setCoupon } from './request'; import { getCoupon, setCoupon } from './request'
import Add from '@/assets/images/icon/add.png'; import Add from '@/assets/images/icon/add.png'
import coupon from '@/assets/images/icon/coupon.png'; import coupon from '@/assets/images/icon/coupon.png'
import coupon2 from '@/assets/images/icon/coupon2.png'; import coupon2 from '@/assets/images/icon/coupon2.png'
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux'
import AppList from '@/components/AppList'; import AppList from '@/components/list'
import FormDrawer from './components/FormDrawer'; import FormDrawer from './components/FormDrawer'
import SendCouponDrawer from './components/SendCouponDrawer'; import SendCouponDrawer from './components/SendCouponDrawer'
import * as api from './api'; import * as api from './api'
const Coupon = () => { const Coupon = () => {
const { modal } = App.useApp(); const { modal } = App.useApp()
const appListRef = useRef(null); const appListRef = useRef(null)
const reload = async () => { const reload = async () => {
await appListRef.current.handleReload(true); await appListRef.current.handleReload(true)
}; }
const [drawerOpen, setDrawerOpen] = useState(false); const [drawerOpen, setDrawerOpen] = useState(false)
// 获取操作权限 // 获取操作权限
const { operationPermissionsList } = useSelector((state) => state.user); const { operationPermissionsList } = useSelector(state => state.user)
// 操作某一项的id // 操作某一项的id
const [id, setId] = useState(''); const [id, setId] = useState('')
const [switchFlag, setSwitchFlag] = useState(false); const [switchFlag, setSwitchFlag] = useState(false)
const [couponList, setCouponList] = useState([]); const [couponList, setCouponList] = useState([])
const [sendCouponDrawerOpen, setSendCouponDrawerOpen] = useState(false); const [sendCouponDrawerOpen, setSendCouponDrawerOpen] = useState(false)
// 添加 // 添加
const handleAdd = () => { const handleAdd = () => {
setId(''); setId('')
setDrawerOpen(true); setDrawerOpen(true)
}; }
// 编辑 // 编辑
const handleEdit = async (id) => { const handleEdit = async id => {
setId(id); setId(id)
setDrawerOpen(true); setDrawerOpen(true)
}; }
const onClose = () => { const onClose = () => {
setDrawerOpen(false); setDrawerOpen(false)
}; }
// 删除 // 删除
const handleDelete = async (id) => { const handleDelete = async id => {
modal.confirm({ modal.confirm({
title: '提示', title: '提示',
content: '优惠券将永久删除,是否继续?', content: '优惠券将永久删除,是否继续?',
onOk: async () => { onOk: async () => {
await api.delCoupon({ id }); await api.delCoupon({ id })
reload(); reload()
}, }
}); })
}; }
// 获取优惠券开关状态 // 获取优惠券开关状态
const handleCoupon = async () => { const handleCoupon = async () => {
const { coupon_switch } = await getCoupon(); const { coupon_switch } = await getCoupon()
coupon_switch == '1' ? setSwitchFlag(true) : setSwitchFlag(false); coupon_switch == '1' ? setSwitchFlag(true) : setSwitchFlag(false)
}; }
const [swithcLoad, setSwitchLoad] = useState(false); const [swithcLoad, setSwitchLoad] = useState(false)
// 改变优惠券开关 // 改变优惠券开关
const changeCoupon = async (val) => { const changeCoupon = async val => {
setSwitchLoad(true); setSwitchLoad(true)
let coupon_switch = val ? '1' : '0'; let coupon_switch = val ? '1' : '0'
const bool = await setCoupon({ coupon_switch }); const bool = await setCoupon({ coupon_switch })
setTimeout(() => { setTimeout(() => {
setSwitchLoad(false); setSwitchLoad(false)
}, 500); }, 500)
handleCoupon(); handleCoupon()
}; }
useEffect(() => { useEffect(() => {
handleCoupon(); handleCoupon()
}, []); }, [])
const remote = { const remote = {
request: api.getList, request: api.getList,
afterRequest(data) { afterRequest(data) {
setCouponList(data.list); setCouponList(data.list)
return data; return data
}, }
}; }
const filters = [ const filters = [
{ {
label: '优惠券名称', label: '优惠券名称',
name: 'name', name: 'name',
element: <Input placeholder='请输入优惠券名称' allowClear />, element: <Input placeholder="请输入优惠券名称" allowClear />
}, }
]; ]
const columns = [ const columns = [
{ {
title: 'ID', title: 'ID',
key: 'id', key: 'id',
align: 'center', align: 'center',
dataIndex: 'id', dataIndex: 'id'
}, },
{ {
title: '优惠券名称', title: '优惠券名称',
...@@ -100,7 +100,7 @@ const Coupon = () => { ...@@ -100,7 +100,7 @@ const Coupon = () => {
align: 'center', align: 'center',
dataIndex: 'name', dataIndex: 'name',
width: 200, width: 200,
render: (text) => ( render: text => (
<Popover content={text} overlayStyle={{ maxWidth: '400px' }}> <Popover content={text} overlayStyle={{ maxWidth: '400px' }}>
<div <div
style={{ style={{
...@@ -109,49 +109,48 @@ const Coupon = () => { ...@@ -109,49 +109,48 @@ const Coupon = () => {
WebkitBoxOrient: 'vertical', WebkitBoxOrient: 'vertical',
overflow: 'hidden', overflow: 'hidden',
textOverflow: 'ellipsis', textOverflow: 'ellipsis',
textAlign: 'center', textAlign: 'center'
}} }}>
>
{text} {text}
</div> </div>
</Popover> </Popover>
), )
}, },
{ {
title: '达标金额', title: '达标金额',
key: 'norm_price', key: 'norm_price',
align: 'center', align: 'center',
dataIndex: 'norm_price', dataIndex: 'norm_price'
}, },
{ {
title: '满减金额', title: '满减金额',
key: 'reduced_price', key: 'reduced_price',
align: 'center', align: 'center',
dataIndex: 'reduced_price', dataIndex: 'reduced_price'
}, },
{ {
title: '有效期开始时间', title: '有效期开始时间',
key: 'start_time', key: 'start_time',
align: 'center', align: 'center',
dataIndex: 'start_time', dataIndex: 'start_time'
}, },
{ {
title: '有效期结束时间', title: '有效期结束时间',
key: 'end_time', key: 'end_time',
align: 'center', align: 'center',
dataIndex: 'end_time', dataIndex: 'end_time'
}, },
{ {
title: '领取人数', title: '领取人数',
key: 'receive_num', key: 'receive_num',
align: 'center', align: 'center',
dataIndex: 'receive_num', dataIndex: 'receive_num'
}, },
{ {
title: '使用人数', title: '使用人数',
key: 'use_num', key: 'use_num',
align: 'center', align: 'center',
dataIndex: 'use_num', dataIndex: 'use_num'
}, },
{ {
title: '操作', title: '操作',
...@@ -161,30 +160,25 @@ const Coupon = () => { ...@@ -161,30 +160,25 @@ const Coupon = () => {
render: (_, { id }) => { render: (_, { id }) => {
return ( return (
<Space> <Space>
{operationPermissionsList.includes('/setting/coupon/getInfoById') && ( {operationPermissionsList.includes('/setting/coupon/getInfoById') && <Button onClick={() => handleEdit(id)}>编辑</Button>}
<Button onClick={() => handleEdit(id)}>编辑</Button> {operationPermissionsList.includes('/setting/coupon/del') && <Button onClick={() => handleDelete(id)}>删除</Button>}
)}
{operationPermissionsList.includes('/setting/coupon/del') && (
<Button onClick={() => handleDelete(id)}>删除</Button>
)}
</Space> </Space>
); )
}, }
}, }
]; ]
const filterAside = ( const filterAside = (
<Space> <Space>
<label htmlFor='coupon'>优惠券开关</label> <label htmlFor="coupon">优惠券开关</label>
<Switch <Switch
checked={switchFlag} checked={switchFlag}
loading={swithcLoad} loading={swithcLoad}
onChange={changeCoupon} onChange={changeCoupon}
disabled={!operationPermissionsList.includes('/setting/coupon/getCoupon')} disabled={!operationPermissionsList.includes('/setting/coupon/getCoupon')}></Switch>
></Switch>
{operationPermissionsList.includes('/setting/coupon/add') && ( {operationPermissionsList.includes('/setting/coupon/add') && (
<Button <Button
type='primary' type="primary"
disabled={!switchFlag} disabled={!switchFlag}
ghost ghost
icon={ icon={
...@@ -194,19 +188,18 @@ const Coupon = () => { ...@@ -194,19 +188,18 @@ const Coupon = () => {
display: 'inline-block', display: 'inline-block',
width: '13px', width: '13px',
height: '11px', height: '11px',
pointerEvents: 'none', pointerEvents: 'none'
}} }}
src={!switchFlag ? coupon2 : coupon} src={!switchFlag ? coupon2 : coupon}
/> />
} }
onClick={() => setSendCouponDrawerOpen(true)} onClick={() => setSendCouponDrawerOpen(true)}>
>
优惠券发放 优惠券发放
</Button> </Button>
)} )}
{operationPermissionsList.includes('/setting/coupon/give') && ( {operationPermissionsList.includes('/setting/coupon/give') && (
<Button <Button
type='primary' type="primary"
ghost ghost
icon={ icon={
<span <span
...@@ -214,33 +207,27 @@ const Coupon = () => { ...@@ -214,33 +207,27 @@ const Coupon = () => {
display: 'inline-block', display: 'inline-block',
width: '13px', width: '13px',
height: '12px', height: '12px',
pointerEvents: 'none', pointerEvents: 'none'
}} }}>
>
<Image src={Add} /> <Image src={Add} />
</span> </span>
} }
onClick={handleAdd} onClick={handleAdd}>
>
添加 添加
</Button> </Button>
)} )}
</Space> </Space>
); )
return ( return (
<div className='classify'> <div className="classify">
<AppList {...{ remote, filters, columns, filterAside }} ref={appListRef}></AppList> <AppList {...{ remote, filters, columns, filterAside }} ref={appListRef}></AppList>
{/* 添加编辑 */} {/* 添加编辑 */}
<FormDrawer open={drawerOpen} id={id} onClose={onClose} onComplete={reload}></FormDrawer> <FormDrawer open={drawerOpen} id={id} onClose={onClose} onComplete={reload}></FormDrawer>
{/* 优惠券发送 */} {/* 优惠券发送 */}
<SendCouponDrawer <SendCouponDrawer open={sendCouponDrawerOpen} couponList={couponList} onClose={() => setSendCouponDrawerOpen(false)}></SendCouponDrawer>
open={sendCouponDrawerOpen}
couponList={couponList}
onClose={() => setSendCouponDrawerOpen(false)}
></SendCouponDrawer>
</div> </div>
); )
}; }
export default Coupon; export default Coupon
import { Input, DatePicker, Popover } from 'antd'; import { Input, DatePicker, Popover } from 'antd'
import AppList from '@/components/AppList'; import AppList from '@/components/list'
import { getList } from './api'; import { getList } from './api'
const { RangePicker } = DatePicker; const { RangePicker } = DatePicker
export default function FeedBack() { export default function FeedBack() {
const remote = { const remote = {
request: getList, request: getList,
beforeRequest(params) { beforeRequest(params) {
const { time, ...rest } = params; const { time, ...rest } = params
if (time) { if (time) {
rest.start_time = new Date(time[0]).getTime() / 1000; rest.start_time = new Date(time[0]).getTime() / 1000
rest.end_time = new Date(time[1]).getTime() / 1000; rest.end_time = new Date(time[1]).getTime() / 1000
}
return rest
}
} }
return rest;
},
};
const filters = [ const filters = [
{ {
label: '用户名称', label: '用户名称',
name: 'name', name: 'name',
element: <Input placeholder='请输入用户名称' allowClear />, element: <Input placeholder="请输入用户名称" allowClear />
}, },
{ {
label: '手机号', label: '手机号',
name: 'phone', name: 'phone',
element: <Input placeholder='请输入手机号' allowClear />, element: <Input placeholder="请输入手机号" allowClear />
}, },
{ {
label: '反馈时间', label: '反馈时间',
name: 'time', name: 'time',
element: <RangePicker allowClear />, element: <RangePicker allowClear />
}, }
]; ]
const columns = [ const columns = [
{ {
title: 'ID', title: 'ID',
key: 'id', key: 'id',
dataIndex: 'id', dataIndex: 'id'
}, },
{ {
title: '用户名称', title: '用户名称',
key: 'name', key: 'name',
dataIndex: 'name', dataIndex: 'name',
align: 'center', align: 'center'
}, },
{ {
title: '手机号', title: '手机号',
key: 'phone', key: 'phone',
dataIndex: 'phone', dataIndex: 'phone',
align: 'center', align: 'center'
}, },
{ {
title: '反馈时间', title: '反馈时间',
key: 'create_time', key: 'create_time',
dataIndex: 'create_time', dataIndex: 'create_time',
align: 'center', align: 'center'
}, },
{ {
title: '反馈内容', title: '反馈内容',
...@@ -62,7 +62,7 @@ export default function FeedBack() { ...@@ -62,7 +62,7 @@ export default function FeedBack() {
dataIndex: 'proposal', dataIndex: 'proposal',
align: 'center', align: 'center',
width: 400, width: 400,
render: (text) => ( render: text => (
<Popover content={text} overlayStyle={{ maxWidth: '400px' }}> <Popover content={text} overlayStyle={{ maxWidth: '400px' }}>
<div <div
style={{ style={{
...@@ -71,15 +71,14 @@ export default function FeedBack() { ...@@ -71,15 +71,14 @@ export default function FeedBack() {
WebkitBoxOrient: 'vertical', WebkitBoxOrient: 'vertical',
overflow: 'hidden', overflow: 'hidden',
textOverflow: 'ellipsis', textOverflow: 'ellipsis',
textAlign: 'center', textAlign: 'center'
}} }}>
>
{text} {text}
</div> </div>
</Popover> </Popover>
), )
}, }
]; ]
return <AppList {...{ remote, filters, columns }}></AppList>; return <AppList {...{ remote, filters, columns }}></AppList>
} }
import axios from 'axios'; import axios from 'axios'
import md5 from 'js-md5'; import md5 from 'js-md5'
import qs from 'qs'; import qs from 'qs'
import { Modal, notification } from 'antd'; import { Modal, notification } from 'antd'
function getToken() { function getToken() {
return window.localStorage.getItem('kiwi.token') ? window.localStorage.getItem('kiwi.token') : ''; return window.localStorage.getItem('kiwi.token') ? window.localStorage.getItem('kiwi.token') : ''
} }
function showLoginModal() { function showLoginModal() {
Modal.warning({ Modal.warning({
title: '登录状态已过期,请重新登录', title: '登录状态已过期,请重新登录',
onOk() { onOk() {
localStorage.removeItem('kiwi.gpt.token'); localStorage.removeItem('kiwi.gpt.token')
location.href = '/login'; location.href = '/login'
}, }
}); })
} }
// 排序 // 排序
function alphabeticalSort(a, b) { function alphabeticalSort(a, b) {
return a.localeCompare(b); return a.localeCompare(b)
} }
const httpRequest = axios.create({ const httpRequest = axios.create({
// timeout: 60000, // timeout: 60000,
withCredentials: true, withCredentials: true
}); })
// 请求拦截器 // 请求拦截器
httpRequest.interceptors.request.use( httpRequest.interceptors.request.use(
(config) => { config => {
const token = getToken(); const token = getToken()
// 应用设置 // 应用设置
const appId = 'TzEU5jPk2tu80266'; const appId = 'TzEU5jPk2tu80266'
const appSecret = '0a006048a4480481b18fef1405120b83'; const appSecret = '0a006048a4480481b18fef1405120b83'
const timestamp = Math.floor(Date.now() / 1000); const timestamp = Math.floor(Date.now() / 1000)
const signData = { const signData = {
...config.data, ...config.data,
...@@ -42,60 +42,60 @@ httpRequest.interceptors.request.use( ...@@ -42,60 +42,60 @@ httpRequest.interceptors.request.use(
appSecret, appSecret,
timestamp, timestamp,
token, token,
url: config.url, url: config.url
}; }
const signStr = qs.stringify(signData, { sort: alphabeticalSort }); const signStr = qs.stringify(signData, { sort: alphabeticalSort })
const salt = '&4F6g4Y6b5L4R9'; const salt = '&4F6g4Y6b5L4R9'
const sign = md5(md5(signStr + salt)); const sign = md5(md5(signStr + salt))
const defaultHeaders = { const defaultHeaders = {
Authorization: token, Authorization: token,
AppId: appId, AppId: appId,
AppSecret: appSecret, AppSecret: appSecret,
Timestamp: timestamp, Timestamp: timestamp,
Sign: sign, Sign: sign
}; }
config.headers = { ...config.headers, ...defaultHeaders }; config.headers = { ...config.headers, ...defaultHeaders }
return config; return config
}, },
(error) => { error => {
return Promise.reject(error); return Promise.reject(error)
}, }
); )
// 响应拦截器 // 响应拦截器
httpRequest.interceptors.response.use( httpRequest.interceptors.response.use(
(response) => { response => {
const { data } = response; const { data } = response
if (data.code === 401 || data.code === 403) { if (data.code === 401 || data.code === 403) {
if (location.pathname !== '/login') { if (location.pathname !== '/login') {
showLoginModal(); showLoginModal()
} }
return Promise.reject(data); return Promise.reject(data)
} }
if (data.code !== 200) { if (data.code !== 200) {
notification.error({ message: data.message || '请求错误' }); notification.error({ message: data.message || '请求错误' })
return Promise.reject(data); return Promise.reject(data)
} }
return data; return data
}, },
(error) => { error => {
if (error.response) { if (error.response) {
const { status, data = {} } = error.response; const { status, data = {} } = error.response
if (status === 401 || status === 403) { if (status === 401 || status === 403) {
showLoginModal(); showLoginModal()
} else { } else {
notification.error({ message: data.message || '请求错误' }); notification.error({ message: data.message || '请求错误' })
console.error(`${status}: ${data.message}`); console.error(`${status}: ${data.message}`)
} }
} else { } else {
console.log(error); console.log(error)
} }
return Promise.reject(error.response || error); return Promise.reject(error.response || error)
}, }
); )
export default httpRequest; export default httpRequest
差异被折叠。
...@@ -44,7 +44,8 @@ async function createOSSInstance() { ...@@ -44,7 +44,8 @@ async function createOSSInstance() {
export async function uploadFile(file) { export async function uploadFile(file) {
try { try {
const oss = await createOSSInstance() const oss = await createOSSInstance()
const fileName = `${new Date().getTime()}-${Math.random() * 1000}.jpg` const fileExt = file.name.substring(file.name.lastIndexOf('.'))
const fileName = `${new Date().getTime()}-${Math.random() * 1000}${fileExt}`
const result = await oss.put(fileName, file) const result = await oss.put(fileName, file)
return result.url return result.url
} catch (error) { } catch (error) {
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论