Refactor backend helpers and fix UI warnings
This commit is contained in:
94
back/main.py
94
back/main.py
@@ -25,6 +25,9 @@ NEED_AUDIT = True
|
|||||||
FILE_SIZE_LIMIT_MB = 10.0
|
FILE_SIZE_LIMIT_MB = 10.0
|
||||||
FILE_FORMATS = ["png", "jpg", "jpeg", "gif", "webp"]
|
FILE_FORMATS = ["png", "jpg", "jpeg", "gif", "webp"]
|
||||||
|
|
||||||
|
def now_time():
|
||||||
|
return datetime.now()
|
||||||
|
|
||||||
# --- 定义数据库结构 ---
|
# --- 定义数据库结构 ---
|
||||||
class SiteSettings(db.Model):
|
class SiteSettings(db.Model):
|
||||||
__tablename__ = 'site_settings'
|
__tablename__ = 'site_settings'
|
||||||
@@ -51,8 +54,8 @@ class Submission(db.Model):
|
|||||||
content = db.Column(db.Text, nullable=False)
|
content = db.Column(db.Text, nullable=False)
|
||||||
identity_token = db.Column(db.String(36), nullable=True)
|
identity_token = db.Column(db.String(36), nullable=True)
|
||||||
status = db.Column(db.String(20), default='Pending')
|
status = db.Column(db.String(20), default='Pending')
|
||||||
created_at = db.Column(db.DateTime, default=lambda: datetime.now())
|
created_at = db.Column(db.DateTime, default=now_time)
|
||||||
updated_at = db.Column(db.DateTime, default=lambda: datetime.now())
|
updated_at = db.Column(db.DateTime, default=now_time, onupdate=now_time)
|
||||||
upvotes = db.Column(db.Integer, default=0)
|
upvotes = db.Column(db.Integer, default=0)
|
||||||
downvotes = db.Column(db.Integer, default=0)
|
downvotes = db.Column(db.Integer, default=0)
|
||||||
|
|
||||||
@@ -65,7 +68,7 @@ class Comment(db.Model):
|
|||||||
nickname = db.Column(db.String(50), default='匿名用户')
|
nickname = db.Column(db.String(50), default='匿名用户')
|
||||||
content = db.Column(db.Text, nullable=False)
|
content = db.Column(db.Text, nullable=False)
|
||||||
identity_token = db.Column(db.String(36), nullable=True)
|
identity_token = db.Column(db.String(36), nullable=True)
|
||||||
created_at = db.Column(db.DateTime, default=lambda: datetime.now())
|
created_at = db.Column(db.DateTime, default=now_time)
|
||||||
parent_comment_id = db.Column(db.Integer, db.ForeignKey('comments.id'), nullable=True)
|
parent_comment_id = db.Column(db.Integer, db.ForeignKey('comments.id'), nullable=True)
|
||||||
|
|
||||||
class Report(db.Model):
|
class Report(db.Model):
|
||||||
@@ -80,6 +83,9 @@ class Report(db.Model):
|
|||||||
|
|
||||||
class Hashtag(db.Model):
|
class Hashtag(db.Model):
|
||||||
__tablename__ = 'hashtags'
|
__tablename__ = 'hashtags'
|
||||||
|
__table_args__ = (
|
||||||
|
db.Index('ix_hashtags_type_target', 'type', 'target_id'),
|
||||||
|
)
|
||||||
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
||||||
type = db.Column(db.Integer, nullable=False) # 0: Submission, 1: Comment
|
type = db.Column(db.Integer, nullable=False) # 0: Submission, 1: Comment
|
||||||
target_id = db.Column(db.Integer, nullable=False)
|
target_id = db.Column(db.Integer, nullable=False)
|
||||||
@@ -103,8 +109,8 @@ class SiteNotice(db.Model):
|
|||||||
type = db.Column(db.String(10), default='md', nullable=False)
|
type = db.Column(db.String(10), default='md', nullable=False)
|
||||||
content = db.Column(db.Text, default='', nullable=False)
|
content = db.Column(db.Text, default='', nullable=False)
|
||||||
version = db.Column(db.Integer, default=0, nullable=False)
|
version = db.Column(db.Integer, default=0, nullable=False)
|
||||||
created_at = db.Column(db.DateTime, default=lambda: datetime.now())
|
created_at = db.Column(db.DateTime, default=now_time)
|
||||||
updated_at = db.Column(db.DateTime, default=lambda: datetime.now(), onupdate=datetime.now)
|
updated_at = db.Column(db.DateTime, default=now_time, onupdate=now_time)
|
||||||
|
|
||||||
# 初始化数据库函数
|
# 初始化数据库函数
|
||||||
def init_db():
|
def init_db():
|
||||||
@@ -159,6 +165,38 @@ def load_config():
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Warning: Failed to load deny words: {e}")
|
print(f"Warning: Failed to load deny words: {e}")
|
||||||
|
|
||||||
|
def is_identity_valid(token):
|
||||||
|
if not token:
|
||||||
|
return True
|
||||||
|
return Identity.query.filter_by(token=token).first() is not None
|
||||||
|
|
||||||
|
def normalize_identity(token):
|
||||||
|
if token and not is_identity_valid(token):
|
||||||
|
return False, None
|
||||||
|
return True, token if token else None
|
||||||
|
|
||||||
|
def find_deny_word(content):
|
||||||
|
try:
|
||||||
|
words = DENY_WORDS_CACHE
|
||||||
|
except Exception:
|
||||||
|
words = []
|
||||||
|
for word in words:
|
||||||
|
if word in content:
|
||||||
|
return word
|
||||||
|
return None
|
||||||
|
|
||||||
|
def save_hashtags(tag_type, target_id, hashtopic):
|
||||||
|
if not hashtopic:
|
||||||
|
return
|
||||||
|
for tag in hashtopic:
|
||||||
|
new_tag = Hashtag(
|
||||||
|
type=tag_type,
|
||||||
|
target_id=target_id,
|
||||||
|
name=tag
|
||||||
|
)
|
||||||
|
db.session.add(new_tag)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
# --- 用户普通api端点 ---
|
# --- 用户普通api端点 ---
|
||||||
@app.route('/api/settings', methods=['GET'])
|
@app.route('/api/settings', methods=['GET'])
|
||||||
def get_settings():
|
def get_settings():
|
||||||
@@ -277,19 +315,16 @@ def submit_post():
|
|||||||
identity_token = data.get('identity')
|
identity_token = data.get('identity')
|
||||||
|
|
||||||
# 违禁词检测
|
# 违禁词检测
|
||||||
for word in DENY_WORDS_CACHE:
|
if find_deny_word(content):
|
||||||
if word in content:
|
|
||||||
return jsonify({"code": 2005, "data": "提交内容包含违禁词"})
|
return jsonify({"code": 2005, "data": "提交内容包含违禁词"})
|
||||||
|
|
||||||
# Identity 验证
|
# Identity 验证
|
||||||
if identity_token:
|
ok, identity_token = normalize_identity(identity_token)
|
||||||
if not Identity.query.filter_by(token=identity_token).first():
|
if not ok:
|
||||||
return jsonify({"code": 2004, "data": "无效的 Identity Token"})
|
return jsonify({"code": 2004, "data": "无效的 Identity Token"})
|
||||||
else:
|
|
||||||
identity_token = None
|
|
||||||
|
|
||||||
# 保存
|
# 保存
|
||||||
now = datetime.now()
|
now = now_time()
|
||||||
new_post = Submission(
|
new_post = Submission(
|
||||||
content=content,
|
content=content,
|
||||||
identity_token=identity_token,
|
identity_token=identity_token,
|
||||||
@@ -301,15 +336,7 @@ def submit_post():
|
|||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
# 保存 Hashtags
|
# 保存 Hashtags
|
||||||
if hashtopic:
|
save_hashtags(0, new_post.id, hashtopic)
|
||||||
for tag in hashtopic:
|
|
||||||
new_tag = Hashtag(
|
|
||||||
type=0, # 0 for Submission
|
|
||||||
target_id=new_post.id,
|
|
||||||
name=tag
|
|
||||||
)
|
|
||||||
db.session.add(new_tag)
|
|
||||||
db.session.commit()
|
|
||||||
|
|
||||||
code = 1002 if new_post.status == 'Pending' else 1001
|
code = 1002 if new_post.status == 'Pending' else 1001
|
||||||
return jsonify({"code": code, "data": {"id": new_post.id}})
|
return jsonify({"code": code, "data": {"id": new_post.id}})
|
||||||
@@ -343,16 +370,13 @@ def submit_comment():
|
|||||||
return jsonify({"code": 2002, "data": "投稿不存在"})
|
return jsonify({"code": 2002, "data": "投稿不存在"})
|
||||||
|
|
||||||
# 违禁词检测
|
# 违禁词检测
|
||||||
for word in DENY_WORDS_CACHE:
|
if find_deny_word(content):
|
||||||
if word in content:
|
|
||||||
return jsonify({"code": 2005, "data": "提交内容包含违禁词"})
|
return jsonify({"code": 2005, "data": "提交内容包含违禁词"})
|
||||||
|
|
||||||
# Identity 验证
|
# Identity 验证
|
||||||
if identity_token:
|
ok, identity_token = normalize_identity(identity_token)
|
||||||
if not Identity.query.filter_by(token=identity_token).first():
|
if not ok:
|
||||||
return jsonify({"code": 2004, "data": "无效的 Identity Token"})
|
return jsonify({"code": 2004, "data": "无效的 Identity Token"})
|
||||||
else:
|
|
||||||
identity_token = None
|
|
||||||
|
|
||||||
new_comment = Comment(
|
new_comment = Comment(
|
||||||
submission_id=submission_id,
|
submission_id=submission_id,
|
||||||
@@ -365,15 +389,7 @@ def submit_comment():
|
|||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
# 保存 Hashtags
|
# 保存 Hashtags
|
||||||
if hashtopic:
|
save_hashtags(1, new_comment.id, hashtopic)
|
||||||
for tag in hashtopic:
|
|
||||||
new_tag = Hashtag(
|
|
||||||
type=1, # 1 for Comment
|
|
||||||
target_id=new_comment.id,
|
|
||||||
name=tag
|
|
||||||
)
|
|
||||||
db.session.add(new_tag)
|
|
||||||
db.session.commit()
|
|
||||||
|
|
||||||
return jsonify({"code": 1001, "data": {"id": new_comment.id}})
|
return jsonify({"code": 1001, "data": {"id": new_comment.id}})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -399,11 +415,9 @@ def submit_report():
|
|||||||
return jsonify({"code": 2002, "data": "投稿不存在"})
|
return jsonify({"code": 2002, "data": "投稿不存在"})
|
||||||
|
|
||||||
identity_token = data.get('identity')
|
identity_token = data.get('identity')
|
||||||
if identity_token:
|
ok, identity_token = normalize_identity(identity_token)
|
||||||
if not Identity.query.filter_by(token=identity_token).first():
|
if not ok:
|
||||||
return jsonify({"code": 2004, "data": "无效的Identity Token"})
|
return jsonify({"code": 2004, "data": "无效的Identity Token"})
|
||||||
else:
|
|
||||||
identity_token = None
|
|
||||||
|
|
||||||
report = Report(
|
report = Report(
|
||||||
submission_id=submission_id,
|
submission_id=submission_id,
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
Button,
|
Button,
|
||||||
Text,
|
Text,
|
||||||
makeStyles,
|
makeStyles,
|
||||||
|
mergeClasses,
|
||||||
shorthands,
|
shorthands,
|
||||||
tokens,
|
tokens,
|
||||||
Input,
|
Input,
|
||||||
@@ -672,7 +673,10 @@ const CreatePost: React.FC = () => {
|
|||||||
{suggestions.map((item, idx) => (
|
{suggestions.map((item, idx) => (
|
||||||
<div
|
<div
|
||||||
key={`${item}-${idx}`}
|
key={`${item}-${idx}`}
|
||||||
className={`${styles.suggestItem} ${idx === activeSuggest ? styles.suggestItemActive : ''}`}
|
className={mergeClasses(
|
||||||
|
styles.suggestItem,
|
||||||
|
idx === activeSuggest ? styles.suggestItemActive : undefined
|
||||||
|
)}
|
||||||
onMouseDown={(e) => {
|
onMouseDown={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
handleSuggestApply(item);
|
handleSuggestApply(item);
|
||||||
@@ -744,7 +748,10 @@ const CreatePost: React.FC = () => {
|
|||||||
{tagSuggestions.map((item, idx) => (
|
{tagSuggestions.map((item, idx) => (
|
||||||
<div
|
<div
|
||||||
key={`${item}-${idx}`}
|
key={`${item}-${idx}`}
|
||||||
className={`${styles.suggestItem} ${idx === activeTagSuggest ? styles.suggestItemActive : ''}`}
|
className={mergeClasses(
|
||||||
|
styles.suggestItem,
|
||||||
|
idx === activeTagSuggest ? styles.suggestItemActive : undefined
|
||||||
|
)}
|
||||||
onMouseDown={(e) => {
|
onMouseDown={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
applyTagSuggest(item);
|
applyTagSuggest(item);
|
||||||
|
|||||||
@@ -83,6 +83,7 @@ const ImageViewer: React.FC<ImageViewerProps> = ({ src, alt, onClose }) => {
|
|||||||
const [filename, setFilename] = React.useState<string>('image');
|
const [filename, setFilename] = React.useState<string>('image');
|
||||||
const startRef = React.useRef<{ x: number; y: number }>({ x: 0, y: 0 });
|
const startRef = React.useRef<{ x: number; y: number }>({ x: 0, y: 0 });
|
||||||
const offsetRef = React.useRef<{ x: number; y: number }>({ x: 0, y: 0 });
|
const offsetRef = React.useRef<{ x: number; y: number }>({ x: 0, y: 0 });
|
||||||
|
const imgRef = React.useRef<HTMLImageElement | null>(null);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
setScale(1);
|
setScale(1);
|
||||||
@@ -100,7 +101,7 @@ const ImageViewer: React.FC<ImageViewerProps> = ({ src, alt, onClose }) => {
|
|||||||
.catch(() => setFilename(fallback));
|
.catch(() => setFilename(fallback));
|
||||||
}, [src]);
|
}, [src]);
|
||||||
|
|
||||||
const handleWheel: React.WheelEventHandler<HTMLImageElement> = (e) => {
|
const handleWheel = (e: WheelEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const next = clamp(scale + (e.deltaY < 0 ? 0.15 : -0.15), 0.5, 5);
|
const next = clamp(scale + (e.deltaY < 0 ? 0.15 : -0.15), 0.5, 5);
|
||||||
setScale(next);
|
setScale(next);
|
||||||
@@ -129,6 +130,15 @@ const ImageViewer: React.FC<ImageViewerProps> = ({ src, alt, onClose }) => {
|
|||||||
setOffset({ x: 0, y: 0 });
|
setOffset({ x: 0, y: 0 });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const el = imgRef.current;
|
||||||
|
if (!el) return;
|
||||||
|
el.addEventListener('wheel', handleWheel, { passive: false });
|
||||||
|
return () => {
|
||||||
|
el.removeEventListener('wheel', handleWheel as EventListener);
|
||||||
|
};
|
||||||
|
}, [scale]);
|
||||||
|
|
||||||
const handleDownload = async () => {
|
const handleDownload = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(src, { mode: 'cors' });
|
const res = await fetch(src, { mode: 'cors' });
|
||||||
@@ -161,10 +171,10 @@ const ImageViewer: React.FC<ImageViewerProps> = ({ src, alt, onClose }) => {
|
|||||||
</div>
|
</div>
|
||||||
<div className={styles.viewerContainer} onClick={(e) => e.stopPropagation()}>
|
<div className={styles.viewerContainer} onClick={(e) => e.stopPropagation()}>
|
||||||
<img
|
<img
|
||||||
|
ref={imgRef}
|
||||||
src={src}
|
src={src}
|
||||||
alt={alt || 'image'}
|
alt={alt || 'image'}
|
||||||
className={styles.image}
|
className={styles.image}
|
||||||
onWheel={handleWheel}
|
|
||||||
onMouseDown={handleMouseDown}
|
onMouseDown={handleMouseDown}
|
||||||
onMouseMove={handleMouseMove}
|
onMouseMove={handleMouseMove}
|
||||||
onMouseUp={handleMouseUpOrLeave}
|
onMouseUp={handleMouseUpOrLeave}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
CardFooter,
|
CardFooter,
|
||||||
Button,
|
Button,
|
||||||
tokens,
|
tokens,
|
||||||
|
mergeClasses,
|
||||||
useToastController,
|
useToastController,
|
||||||
Toast,
|
Toast,
|
||||||
ToastTitle,
|
ToastTitle,
|
||||||
@@ -306,7 +307,10 @@ const PostCard = ({
|
|||||||
<Button
|
<Button
|
||||||
icon={<ArrowUp24Regular primaryFill={voteChoice === 'up' ? tokens.colorBrandForegroundLink : undefined} />}
|
icon={<ArrowUp24Regular primaryFill={voteChoice === 'up' ? tokens.colorBrandForegroundLink : undefined} />}
|
||||||
appearance="transparent"
|
appearance="transparent"
|
||||||
className={`${styles.actionButton} ${voteChoice === 'up' ? styles.actionButtonActive : ''}`}
|
className={mergeClasses(
|
||||||
|
styles.actionButton,
|
||||||
|
voteChoice === 'up' ? styles.actionButtonActive : undefined
|
||||||
|
)}
|
||||||
style={voteChoice === 'up' ? { color: tokens.colorBrandForegroundLink } : undefined}
|
style={voteChoice === 'up' ? { color: tokens.colorBrandForegroundLink } : undefined}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
if (hasVoted) {
|
if (hasVoted) {
|
||||||
@@ -339,7 +343,10 @@ const PostCard = ({
|
|||||||
<Button
|
<Button
|
||||||
icon={<ArrowDown24Regular primaryFill={voteChoice === 'down' ? tokens.colorBrandForegroundLink : undefined} />}
|
icon={<ArrowDown24Regular primaryFill={voteChoice === 'down' ? tokens.colorBrandForegroundLink : undefined} />}
|
||||||
appearance="transparent"
|
appearance="transparent"
|
||||||
className={`${styles.actionButton} ${voteChoice === 'down' ? styles.actionButtonActive : ''}`}
|
className={mergeClasses(
|
||||||
|
styles.actionButton,
|
||||||
|
voteChoice === 'down' ? styles.actionButtonActive : undefined
|
||||||
|
)}
|
||||||
style={voteChoice === 'down' ? { color: tokens.colorBrandForegroundLink } : undefined}
|
style={voteChoice === 'down' ? { color: tokens.colorBrandForegroundLink } : undefined}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
if (hasVoted) {
|
if (hasVoted) {
|
||||||
|
|||||||
Reference in New Issue
Block a user