mirror of
https://github.com/wgh136/nysoure.git
synced 2025-12-16 15:51:14 +00:00
Compare commits
8 Commits
17026a74c5
...
4f564da7b3
| Author | SHA1 | Date | |
|---|---|---|---|
| 4f564da7b3 | |||
| 6ed0b45b41 | |||
| 43070cdce3 | |||
| da8d03ec8f | |||
| 64116dddfd | |||
| 97ee74899c | |||
| 3a99d03427 | |||
| aac1992dba |
@@ -38,6 +38,7 @@
|
||||
window.siteInfo = `{{SiteInfo}}`;
|
||||
window.uploadPrompt = `{{UploadPrompt}}`;
|
||||
window.allowNormalUserUpload = `{{AllowNormalUserUpload}}`;
|
||||
window.siteDescription = `{{SiteDescription}}`;
|
||||
</script>
|
||||
<script id="pre_fetch_data"></script>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -6,6 +6,7 @@ interface MyWindow extends Window {
|
||||
siteInfo?: string;
|
||||
uploadPrompt?: string;
|
||||
allowNormalUserUpload?: string;
|
||||
siteDescription?: string;
|
||||
}
|
||||
|
||||
class App {
|
||||
@@ -21,6 +22,8 @@ class App {
|
||||
|
||||
uploadPrompt = "";
|
||||
|
||||
siteDescription = "";
|
||||
|
||||
allowNormalUserUpload = true;
|
||||
|
||||
constructor() {
|
||||
@@ -44,7 +47,9 @@ class App {
|
||||
}
|
||||
this.siteInfo = (window as MyWindow).siteInfo || "";
|
||||
this.uploadPrompt = (window as MyWindow).uploadPrompt || "";
|
||||
// this.allowNormalUserUpload = (window as MyWindow).allowNormalUserUpload === "true";
|
||||
this.siteDescription = (window as MyWindow).siteDescription || "";
|
||||
this.allowNormalUserUpload =
|
||||
(window as MyWindow).allowNormalUserUpload === "true";
|
||||
}
|
||||
|
||||
saveData() {
|
||||
|
||||
@@ -255,6 +255,7 @@ export const i18nData = {
|
||||
"您没有上传文件的权限,请联系管理员。",
|
||||
"Private": "私有",
|
||||
"View {count} more replies": "查看另外 {count} 条回复",
|
||||
"Survival time": "存活时间",
|
||||
},
|
||||
},
|
||||
"zh-TW": {
|
||||
@@ -513,6 +514,7 @@ export const i18nData = {
|
||||
"您沒有上傳檔案的權限,請聯繫管理員。",
|
||||
"Private": "私有",
|
||||
"View {count} more replies": "查看另外 {count} 條回覆",
|
||||
"Survival time": "存活時間",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -138,6 +138,7 @@ export interface CommentWithResource {
|
||||
resource: Resource;
|
||||
content_truncated: boolean;
|
||||
reply_count: number;
|
||||
replies: Comment[];
|
||||
}
|
||||
|
||||
export interface CommentWithRef {
|
||||
@@ -204,3 +205,9 @@ export interface Collection {
|
||||
images: Image[];
|
||||
isPublic: boolean;
|
||||
}
|
||||
|
||||
export interface Statistics {
|
||||
total_resources: number;
|
||||
total_files: number;
|
||||
start_time: number;
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
Activity,
|
||||
CommentWithRef,
|
||||
Collection,
|
||||
Statistics,
|
||||
} from "./models.ts";
|
||||
|
||||
class Network {
|
||||
@@ -795,6 +796,12 @@ class Network {
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async getStatistic(): Promise<Response<Statistics>> {
|
||||
return this._callApi(() =>
|
||||
axios.get(`${this.apiBaseUrl}/config/statistics`),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const network = new Network();
|
||||
|
||||
@@ -2,12 +2,17 @@ import { useEffect, useState } from "react";
|
||||
import ResourcesView from "../components/resources_view.tsx";
|
||||
import { network } from "../network/network.ts";
|
||||
import { app } from "../app.ts";
|
||||
import { Resource, RSort } from "../network/models.ts";
|
||||
import { Resource, RSort, Statistics } from "../network/models.ts";
|
||||
import { useTranslation } from "../utils/i18n";
|
||||
import { useAppContext } from "../components/AppContext.tsx";
|
||||
import Select from "../components/select.tsx";
|
||||
import { useNavigate } from "react-router";
|
||||
import { useNavigator } from "../components/navigator.tsx";
|
||||
import {
|
||||
MdOutlineAccessTime,
|
||||
MdOutlineArchive,
|
||||
MdOutlineClass,
|
||||
} from "react-icons/md";
|
||||
|
||||
export default function HomePage() {
|
||||
useEffect(() => {
|
||||
@@ -33,7 +38,7 @@ export default function HomePage() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<PinnedResources />
|
||||
<HomeHeader />
|
||||
<div className={"flex pt-4 px-4 items-center"}>
|
||||
<Select
|
||||
values={[
|
||||
@@ -64,8 +69,9 @@ export default function HomePage() {
|
||||
|
||||
let cachedPinnedResources: Resource[] | null = null;
|
||||
|
||||
function PinnedResources() {
|
||||
function HomeHeader() {
|
||||
const [pinnedResources, setPinnedResources] = useState<Resource[]>([]);
|
||||
const [statistic, setStatistic] = useState<Statistics | null>(null);
|
||||
const navigator = useNavigator();
|
||||
|
||||
useEffect(() => {
|
||||
@@ -79,9 +85,18 @@ function PinnedResources() {
|
||||
network.getResampledImageUrl(prefetchData.background),
|
||||
);
|
||||
}
|
||||
let ok1 = false;
|
||||
let ok2 = false;
|
||||
if (prefetchData && prefetchData.statistics) {
|
||||
setStatistic(prefetchData.statistics);
|
||||
ok1 = true;
|
||||
}
|
||||
if (prefetchData && prefetchData.pinned) {
|
||||
cachedPinnedResources = prefetchData.pinned;
|
||||
setPinnedResources(cachedPinnedResources!);
|
||||
ok2 = true;
|
||||
}
|
||||
if (ok1 && ok2) {
|
||||
return;
|
||||
}
|
||||
const fetchPinnedResources = async () => {
|
||||
@@ -91,18 +106,30 @@ function PinnedResources() {
|
||||
setPinnedResources(res.data ?? []);
|
||||
}
|
||||
};
|
||||
const fetchStatistics = async () => {
|
||||
const res = await network.getStatistic();
|
||||
if (res.success) {
|
||||
setStatistic(res.data!);
|
||||
}
|
||||
};
|
||||
fetchPinnedResources();
|
||||
}, []);
|
||||
fetchStatistics();
|
||||
}, [navigator]);
|
||||
|
||||
if (pinnedResources.length == 0) {
|
||||
if (pinnedResources.length == 0 || statistic == null) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 p-4">
|
||||
{pinnedResources.map((resource) => (
|
||||
<PinnedResourceItem key={resource.id} resource={resource} />
|
||||
))}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 p-4 gap-4">
|
||||
<PinnedResourceItem resource={pinnedResources[0]} />
|
||||
<div className={"hidden md:block"}>
|
||||
<div className={"card w-full shadow p-4 mb-4 bg-base-100-tr82 h-28"}>
|
||||
<h2 className={"text-lg font-bold pb-2"}>{app.appName}</h2>
|
||||
<p className={"text-xs"}>{app.siteDescription}</p>
|
||||
</div>
|
||||
<StatisticCard statistic={statistic} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -129,7 +156,7 @@ function PinnedResourceItem({ resource }: { resource: Resource }) {
|
||||
<img
|
||||
src={network.getResampledImageUrl(resource.image.id)}
|
||||
alt="cover"
|
||||
className="w-full aspect-[7/3] object-cover"
|
||||
className="w-full h-52 lg:h-60 object-cover"
|
||||
/>
|
||||
</figure>
|
||||
)}
|
||||
@@ -140,3 +167,40 @@ function PinnedResourceItem({ resource }: { resource: Resource }) {
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
function StatisticCard({ statistic }: { statistic: Statistics }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const now = new Date();
|
||||
const createdAt = new Date(statistic.start_time * 1000);
|
||||
const diffTime = Math.abs(now.getTime() - createdAt.getTime());
|
||||
const survivalTime = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
||||
|
||||
return (
|
||||
<div className="stats shadow w-full bg-base-100-tr82">
|
||||
<div className="stat">
|
||||
<div className="stat-figure text-secondary pt-2">
|
||||
<MdOutlineClass size={28} />
|
||||
</div>
|
||||
<div className="stat-title">{t("Resources")}</div>
|
||||
<div className="stat-value">{statistic.total_resources}</div>
|
||||
</div>
|
||||
|
||||
<div className="stat">
|
||||
<div className="stat-figure text-secondary pt-2">
|
||||
<MdOutlineArchive size={28} />
|
||||
</div>
|
||||
<div className="stat-title">{t("Files")}</div>
|
||||
<div className="stat-value">{statistic.total_files}</div>
|
||||
</div>
|
||||
|
||||
<div className="stat">
|
||||
<div className="stat-figure text-accent pt-2">
|
||||
<MdOutlineAccessTime size={28} />
|
||||
</div>
|
||||
<div className="stat-title">{t("Survival time")}</div>
|
||||
<div className="stat-value">{survivalTime}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -61,10 +61,22 @@ func setServerConfig(c fiber.Ctx) error {
|
||||
})
|
||||
}
|
||||
|
||||
func getStatistics(c fiber.Ctx) error {
|
||||
s, err := service.GetStatistic()
|
||||
if err != nil {
|
||||
return model.NewInternalServerError("Failed to get statistics")
|
||||
}
|
||||
return c.JSON(model.Response[*service.Statistic]{
|
||||
Success: true,
|
||||
Data: s,
|
||||
})
|
||||
}
|
||||
|
||||
func AddConfigRoutes(r fiber.Router) {
|
||||
configGroup := r.Group("/config")
|
||||
{
|
||||
configGroup.Get("/", getServerConfig)
|
||||
configGroup.Post("/", setServerConfig)
|
||||
configGroup.Get("/statistics", getStatistics)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,8 +179,14 @@ func DeleteCommentByID(commentID uint) error {
|
||||
if err := tx.Model(&model.User{}).Where("id = ?", comment.UserID).Update("comments_count", gorm.Expr("comments_count - 1")).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Model(&model.Resource{}).Where("id = ?", comment.RefID).Update("comments", gorm.Expr("comments - 1")).Error; err != nil {
|
||||
return err
|
||||
if comment.Type == model.CommentTypeResource {
|
||||
if err := tx.Model(&model.Resource{}).Where("id = ?", comment.RefID).Update("comments", gorm.Expr("comments - 1")).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
} else if comment.Type == model.CommentTypeReply {
|
||||
if err := tx.Model(&model.Comment{}).Where("id = ?", comment.RefID).Update("reply_count", gorm.Expr("reply_count - 1")).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := tx.
|
||||
Where("type = ? and ref_id = ?", model.ActivityTypeNewComment, commentID).
|
||||
|
||||
@@ -239,3 +239,11 @@ func ListUserFiles(userID uint, page, pageSize int) ([]*model.File, int64, error
|
||||
}
|
||||
return files, count, nil
|
||||
}
|
||||
|
||||
func CountFiles() (int64, error) {
|
||||
var count int64
|
||||
if err := db.Model(&model.File{}).Count(&count).Error; err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
|
||||
@@ -461,3 +461,11 @@ func BatchGetResources(ids []uint) ([]model.Resource, error) {
|
||||
|
||||
return resources, nil
|
||||
}
|
||||
|
||||
func CountResources() (int64, error) {
|
||||
var count int64
|
||||
if err := db.Model(&model.Resource{}).Count(&count).Error; err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
|
||||
@@ -158,10 +158,12 @@ func serveIndexHtml(c fiber.Ctx) error {
|
||||
} else if path == "/" || path == "" {
|
||||
pinned, err := service.GetPinnedResources()
|
||||
random, err1 := service.RandomCover()
|
||||
if err == nil && err1 == nil {
|
||||
statistic, err2 := service.GetStatistic()
|
||||
if err == nil && err1 == nil && err2 == nil {
|
||||
preFetchDataJson, _ := json.Marshal(map[string]interface{}{
|
||||
"pinned": pinned,
|
||||
"background": random,
|
||||
"statistic": statistic,
|
||||
})
|
||||
preFetchData = url.PathEscape(string(preFetchDataJson))
|
||||
}
|
||||
@@ -169,6 +171,7 @@ func serveIndexHtml(c fiber.Ctx) error {
|
||||
|
||||
content = strings.ReplaceAll(content, "{{SiteName}}", siteName)
|
||||
content = strings.ReplaceAll(content, "{{Description}}", description)
|
||||
content = strings.ReplaceAll(content, "{{SiteDescription}}", config.ServerDescription())
|
||||
content = strings.ReplaceAll(content, "{{Preview}}", preview)
|
||||
content = strings.ReplaceAll(content, "{{Title}}", title)
|
||||
content = strings.ReplaceAll(content, "{{Url}}", htmlUrl)
|
||||
|
||||
63
server/service/statistic.go
Normal file
63
server/service/statistic.go
Normal file
@@ -0,0 +1,63 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"nysoure/server/dao"
|
||||
"nysoure/server/utils"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Statistic struct {
|
||||
TotalResources int64 `json:"total_resources"`
|
||||
TotalFiles int64 `json:"total_files"`
|
||||
StartTime int64 `json:"start_time"`
|
||||
}
|
||||
|
||||
var (
|
||||
startTime int64
|
||||
cache = utils.NewMemValueCache[*Statistic](1 * time.Minute)
|
||||
)
|
||||
|
||||
func init() {
|
||||
timeFile := utils.GetStoragePath() + "/.start_time"
|
||||
if _, err := os.Stat(timeFile); os.IsNotExist(err) {
|
||||
startTime = time.Now().Unix()
|
||||
str := fmt.Sprintf("%d", startTime)
|
||||
err := os.WriteFile(timeFile, []byte(str), 0644)
|
||||
if err != nil {
|
||||
panic("Failed to write start time file: " + err.Error())
|
||||
}
|
||||
} else {
|
||||
data, err := os.ReadFile(timeFile)
|
||||
if err != nil {
|
||||
panic("Failed to read start time file: " + err.Error())
|
||||
}
|
||||
var t int64
|
||||
_, err = fmt.Sscanf(string(data), "%d", &t)
|
||||
if err != nil {
|
||||
panic("Failed to parse start time: " + err.Error())
|
||||
}
|
||||
startTime = t
|
||||
}
|
||||
}
|
||||
|
||||
func getStatistic() (*Statistic, error) {
|
||||
totalResources, err := dao.CountResources()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
totalFiles, err := dao.CountFiles()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Statistic{
|
||||
TotalResources: totalResources,
|
||||
TotalFiles: totalFiles,
|
||||
StartTime: startTime,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func GetStatistic() (*Statistic, error) {
|
||||
return cache.Get(getStatistic)
|
||||
}
|
||||
29
server/utils/cache.go
Normal file
29
server/utils/cache.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package utils
|
||||
|
||||
import "time"
|
||||
|
||||
type MemValueCache[T any] struct {
|
||||
value T
|
||||
duration time.Duration
|
||||
expiry time.Time
|
||||
}
|
||||
|
||||
func NewMemValueCache[T any](duration time.Duration) *MemValueCache[T] {
|
||||
return &MemValueCache[T]{
|
||||
duration: duration,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *MemValueCache[T]) Get(fetchFunc func() (T, error)) (T, error) {
|
||||
var zero T
|
||||
if time.Now().Before(c.expiry) {
|
||||
return c.value, nil
|
||||
}
|
||||
v, err := fetchFunc()
|
||||
if err != nil {
|
||||
return zero, err
|
||||
}
|
||||
c.value = v
|
||||
c.expiry = time.Now().Add(c.duration)
|
||||
return v, nil
|
||||
}
|
||||
Reference in New Issue
Block a user