Refactor backend helpers and fix UI warnings

This commit is contained in:
LeonspaceX
2026-01-30 23:55:36 +08:00
parent 86baccd2ee
commit 8e561a2eb7
4 changed files with 89 additions and 51 deletions

View File

@@ -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():
@@ -158,6 +164,38 @@ def load_config():
print(f"Loaded {len(DENY_WORDS_CACHE)} deny words.") print(f"Loaded {len(DENY_WORDS_CACHE)} deny words.")
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'])
@@ -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,

View File

@@ -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);

View File

@@ -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}

View File

@@ -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) {