mirror of https://github.com/chaitin/PandaWiki.git
commit
2d7c20c799
|
|
@ -30,6 +30,7 @@ type NodeDetailResp struct {
|
|||
CreatorAccount string `json:"creator_account"`
|
||||
EditorAccount string `json:"editor_account"`
|
||||
PublisherAccount string `json:"publisher_account" gorm:"-"`
|
||||
PV int64 `json:"pv" gorm:"-"`
|
||||
}
|
||||
|
||||
type NodePermissionReq struct {
|
||||
|
|
|
|||
|
|
@ -25,4 +25,5 @@ type ShareNodeDetailResp struct {
|
|||
EditorAccount string `json:"editor_account"`
|
||||
PublisherAccount string `json:"publisher_account"`
|
||||
List []*domain.ShareNodeDetailItem `json:"list" gorm:"-"`
|
||||
PV int64 `json:"pv" gorm:"-"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4655,6 +4655,9 @@ const docTemplate = `{
|
|||
"search_placeholder": {
|
||||
"type": "string"
|
||||
},
|
||||
"stats_setting": {
|
||||
"$ref": "#/definitions/domain.StatsSetting"
|
||||
},
|
||||
"theme_and_style": {
|
||||
"$ref": "#/definitions/domain.ThemeAndStyle"
|
||||
},
|
||||
|
|
@ -4933,6 +4936,9 @@ const docTemplate = `{
|
|||
"search_placeholder": {
|
||||
"type": "string"
|
||||
},
|
||||
"stats_setting": {
|
||||
"$ref": "#/definitions/domain.StatsSetting"
|
||||
},
|
||||
"theme_and_style": {
|
||||
"$ref": "#/definitions/domain.ThemeAndStyle"
|
||||
},
|
||||
|
|
@ -7377,6 +7383,14 @@ const docTemplate = `{
|
|||
"StatPageSceneLogin"
|
||||
]
|
||||
},
|
||||
"domain.StatsSetting": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"pv_enable": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"domain.SwitchModeReq": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
|
|
@ -8573,6 +8587,9 @@ const docTemplate = `{
|
|||
"publisher_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"pv": {
|
||||
"type": "integer"
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/definitions/domain.NodeStatus"
|
||||
},
|
||||
|
|
@ -8750,6 +8767,9 @@ const docTemplate = `{
|
|||
"publisher_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"pv": {
|
||||
"type": "integer"
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/definitions/domain.NodeStatus"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -4648,6 +4648,9 @@
|
|||
"search_placeholder": {
|
||||
"type": "string"
|
||||
},
|
||||
"stats_setting": {
|
||||
"$ref": "#/definitions/domain.StatsSetting"
|
||||
},
|
||||
"theme_and_style": {
|
||||
"$ref": "#/definitions/domain.ThemeAndStyle"
|
||||
},
|
||||
|
|
@ -4926,6 +4929,9 @@
|
|||
"search_placeholder": {
|
||||
"type": "string"
|
||||
},
|
||||
"stats_setting": {
|
||||
"$ref": "#/definitions/domain.StatsSetting"
|
||||
},
|
||||
"theme_and_style": {
|
||||
"$ref": "#/definitions/domain.ThemeAndStyle"
|
||||
},
|
||||
|
|
@ -7370,6 +7376,14 @@
|
|||
"StatPageSceneLogin"
|
||||
]
|
||||
},
|
||||
"domain.StatsSetting": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"pv_enable": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"domain.SwitchModeReq": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
|
|
@ -8566,6 +8580,9 @@
|
|||
"publisher_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"pv": {
|
||||
"type": "integer"
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/definitions/domain.NodeStatus"
|
||||
},
|
||||
|
|
@ -8743,6 +8760,9 @@
|
|||
"publisher_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"pv": {
|
||||
"type": "integer"
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/definitions/domain.NodeStatus"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -506,6 +506,8 @@ definitions:
|
|||
type: array
|
||||
search_placeholder:
|
||||
type: string
|
||||
stats_setting:
|
||||
$ref: '#/definitions/domain.StatsSetting'
|
||||
theme_and_style:
|
||||
$ref: '#/definitions/domain.ThemeAndStyle'
|
||||
theme_mode:
|
||||
|
|
@ -680,6 +682,8 @@ definitions:
|
|||
type: array
|
||||
search_placeholder:
|
||||
type: string
|
||||
stats_setting:
|
||||
$ref: '#/definitions/domain.StatsSetting'
|
||||
theme_and_style:
|
||||
$ref: '#/definitions/domain.ThemeAndStyle'
|
||||
theme_mode:
|
||||
|
|
@ -2277,6 +2281,11 @@ definitions:
|
|||
- StatPageSceneNodeDetail
|
||||
- StatPageSceneChat
|
||||
- StatPageSceneLogin
|
||||
domain.StatsSetting:
|
||||
properties:
|
||||
pv_enable:
|
||||
type: boolean
|
||||
type: object
|
||||
domain.SwitchModeReq:
|
||||
properties:
|
||||
auto_mode_api_key:
|
||||
|
|
@ -3064,6 +3073,8 @@ definitions:
|
|||
type: string
|
||||
publisher_id:
|
||||
type: string
|
||||
pv:
|
||||
type: integer
|
||||
status:
|
||||
$ref: '#/definitions/domain.NodeStatus'
|
||||
type:
|
||||
|
|
@ -3184,6 +3195,8 @@ definitions:
|
|||
type: string
|
||||
publisher_id:
|
||||
type: string
|
||||
pv:
|
||||
type: integer
|
||||
status:
|
||||
$ref: '#/definitions/domain.NodeStatus'
|
||||
type:
|
||||
|
|
|
|||
|
|
@ -66,8 +66,6 @@ func (t AppType) ToSourceType() consts.SourceType {
|
|||
return consts.SourceTypeOpenAIAPI
|
||||
case AppTypeLarkBot:
|
||||
return consts.SourceTypeLarkBot
|
||||
case AppTypeMcpServer:
|
||||
return consts.SourceTypeMcpServer
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
|
|
@ -172,6 +170,11 @@ type AppSettings struct {
|
|||
ConversationSetting ConversationSetting `json:"conversation_setting"`
|
||||
// MCP Server Settings
|
||||
MCPServerSettings MCPServerSettings `json:"mcp_server_settings,omitempty"`
|
||||
StatsSetting StatsSetting `json:"stats_setting"`
|
||||
}
|
||||
|
||||
type StatsSetting struct {
|
||||
PVEnable bool `json:"pv_enable"`
|
||||
}
|
||||
|
||||
type ConversationSetting struct {
|
||||
|
|
@ -561,6 +564,7 @@ type AppSettingsResp struct {
|
|||
ConversationSetting ConversationSetting `json:"conversation_setting"`
|
||||
// MCP Server Settings
|
||||
MCPServerSettings MCPServerSettings `json:"mcp_server_settings,omitempty"`
|
||||
StatsSetting StatsSetting `json:"stats_setting"`
|
||||
}
|
||||
|
||||
type WebAppLandingConfigResp struct {
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ type BaseEditionLimitation struct {
|
|||
AllowCopyProtection bool `json:"allow_copy_protection"` // 支持内容复制保护
|
||||
AllowOpenAIBotSettings bool `json:"allow_open_ai_bot_settings"` // 支持问答机器人
|
||||
AllowMCPServer bool `json:"allow_mcp_server"` // 支持创建MCP Server
|
||||
AllowNodeStats bool `json:"allow_node_stats"` // 支持文档统计
|
||||
}
|
||||
|
||||
var baseEditionLimitationDefault = BaseEditionLimitation{
|
||||
|
|
|
|||
|
|
@ -105,3 +105,14 @@ type StatPageHour struct {
|
|||
func (StatPageHour) TableName() string {
|
||||
return "stat_page_hours"
|
||||
}
|
||||
|
||||
// NodeStats node表统计数据
|
||||
type NodeStats struct {
|
||||
ID int64 `json:"id" gorm:"primaryKey;autoIncrement"`
|
||||
NodeID string `json:"node_id" gorm:"uniqueIndex"`
|
||||
PV int64 `json:"pv"`
|
||||
}
|
||||
|
||||
func (NodeStats) TableName() string {
|
||||
return "node_stats"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package mq
|
|||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/robfig/cron/v3"
|
||||
|
||||
|
|
@ -66,6 +67,16 @@ func NewStatCronHandler(logger *log.Logger, statRepo *pg.StatRepository, statUse
|
|||
|
||||
func (h *CronHandler) RemoveOldStatData() {
|
||||
h.logger.Info("remove old stat data start")
|
||||
|
||||
// 零点时同步数据至node_stats持久化
|
||||
if time.Now().Hour() == 0 {
|
||||
if err := h.statUseCase.MigrateYesterdayPVToNodeStats(context.Background()); err != nil {
|
||||
h.logger.Error("migrate yesterday PV data to node_stats failed", log.Error(err))
|
||||
} else {
|
||||
h.logger.Info("migrate yesterday PV data to node_stats successful")
|
||||
}
|
||||
}
|
||||
|
||||
err := h.statRepo.RemoveOldData(context.Background())
|
||||
if err != nil {
|
||||
h.logger.Error("remove old stat data failed", log.Error(err))
|
||||
|
|
|
|||
|
|
@ -146,6 +146,7 @@ func (h *NodeHandler) GetNodeDetail(c echo.Context) error {
|
|||
|
||||
node, err := h.usecase.GetNodeByKBID(c.Request().Context(), req.ID, req.KbId, req.Format)
|
||||
if err != nil {
|
||||
h.logger.Error("get node by kb id failed", log.Error(err))
|
||||
return h.NewResponseWithError(c, "get node detail failed", err)
|
||||
}
|
||||
return h.NewResponseWithData(c, node)
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
Subproject commit 7771d31053770721d75e194d1c6e7a4b60fb58aa
|
||||
Subproject commit 93a303e5d55e5fece36675283ba11b81bb79d8db
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
package pg
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/chaitin/panda-wiki/domain"
|
||||
"github.com/chaitin/panda-wiki/utils"
|
||||
)
|
||||
|
||||
func (r *NodeRepository) GetNodeStatsByNodeId(ctx context.Context, nodeId string) (*domain.NodeStats, error) {
|
||||
var nodeStats *domain.NodeStats
|
||||
if err := r.db.WithContext(ctx).
|
||||
Model(&domain.NodeStats{}).
|
||||
Where("node_id = ?", nodeId).
|
||||
First(&nodeStats).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
nodeStats = &domain.NodeStats{
|
||||
ID: 0,
|
||||
NodeID: nodeId,
|
||||
PV: 0,
|
||||
}
|
||||
} else {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
var todayStats int64
|
||||
if err := r.db.WithContext(ctx).Model(&domain.StatPage{}).
|
||||
Where("created_at >= ?", utils.GetTimeHourOffset(-24)).
|
||||
Where("node_id = ?", nodeId).Count(&todayStats).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
nodeStats.PV += todayStats
|
||||
|
||||
return nodeStats, nil
|
||||
}
|
||||
|
|
@ -3,6 +3,9 @@ package pg
|
|||
import (
|
||||
"context"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
|
||||
v1 "github.com/chaitin/panda-wiki/api/stat/v1"
|
||||
"github.com/chaitin/panda-wiki/domain"
|
||||
"github.com/chaitin/panda-wiki/store/cache"
|
||||
|
|
@ -156,3 +159,46 @@ func (r *StatRepository) RemoveOldData(ctx context.Context) error {
|
|||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetYesterdayPVByNode 获取昨天的PV数据,按node_id分组
|
||||
func (r *StatRepository) GetYesterdayPVByNode(ctx context.Context) (map[string]int64, error) {
|
||||
type PVResult struct {
|
||||
NodeID string
|
||||
Count int64
|
||||
}
|
||||
|
||||
var results []PVResult
|
||||
if err := r.db.WithContext(ctx).Model(&domain.StatPage{}).
|
||||
Where("created_at < ?", utils.GetTimeHourOffset(0)).
|
||||
Where("created_at >= ?", utils.GetTimeHourOffset(-24)).
|
||||
Where("node_id != ?", "").
|
||||
Group("node_id").
|
||||
Select("node_id, COUNT(*) as count").
|
||||
Find(&results).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pvMap := make(map[string]int64)
|
||||
for _, result := range results {
|
||||
pvMap[result.NodeID] = result.Count
|
||||
}
|
||||
return pvMap, nil
|
||||
}
|
||||
|
||||
// UpsertNodeStats 插入或更新node_stats表
|
||||
func (r *StatRepository) UpsertNodeStats(ctx context.Context, nodeID string, pvCount int64) error {
|
||||
nodeStats := &domain.NodeStats{
|
||||
NodeID: nodeID,
|
||||
PV: pvCount,
|
||||
}
|
||||
|
||||
// 使用GORM的Clauses进行upsert操作
|
||||
return r.db.WithContext(ctx).
|
||||
Clauses(clause.OnConflict{
|
||||
Columns: []clause.Column{{Name: "node_id"}},
|
||||
DoUpdates: clause.Assignments(map[string]interface{}{
|
||||
"pv": gorm.Expr("node_stats.pv + ?", pvCount),
|
||||
}),
|
||||
}).
|
||||
Create(nodeStats).Error
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
DROP TABLE IF EXISTS node_stats;
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
CREATE TABLE IF NOT EXISTS node_stats (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
node_id TEXT NOT NULL UNIQUE,
|
||||
pv BIGINT NOT NULL DEFAULT 0,
|
||||
created_at timestamptz NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
|
|
@ -534,6 +534,7 @@ func (u *AppUsecase) GetAppDetailByKBIDAndAppType(ctx context.Context, kbID stri
|
|||
WecomAIBotSettings: app.Settings.WecomAIBotSettings,
|
||||
|
||||
MCPServerSettings: app.Settings.MCPServerSettings,
|
||||
StatsSetting: app.Settings.StatsSetting,
|
||||
}
|
||||
|
||||
if !domain.GetBaseEditionLimitation(ctx).AllowCustomCopyright {
|
||||
|
|
@ -651,6 +652,7 @@ func (u *AppUsecase) ShareGetWebAppInfo(ctx context.Context, kbID string, authId
|
|||
ContributeSettings: app.Settings.ContributeSettings,
|
||||
HomePageSetting: app.Settings.HomePageSetting,
|
||||
ConversationSetting: app.Settings.ConversationSetting,
|
||||
StatsSetting: app.Settings.StatsSetting,
|
||||
},
|
||||
}
|
||||
// init ai feedback string
|
||||
|
|
|
|||
|
|
@ -119,6 +119,12 @@ func (u *NodeUsecase) GetNodeByKBID(ctx context.Context, id, kbId, format string
|
|||
node.PublisherAccount = nodeRelease.PublisherAccount
|
||||
}
|
||||
|
||||
nodeStat, err := u.nodeRepo.GetNodeStatsByNodeId(ctx, node.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
node.PV = nodeStat.PV
|
||||
|
||||
if node.Meta.ContentType == domain.ContentTypeMD {
|
||||
return node, nil
|
||||
}
|
||||
|
|
@ -221,6 +227,21 @@ func (u *NodeUsecase) GetNodeReleaseDetailByKBIDAndID(ctx context.Context, kbID,
|
|||
node.PublisherAccount = account
|
||||
}
|
||||
|
||||
if domain.GetBaseEditionLimitation(ctx).AllowNodeStats {
|
||||
webApp, err := u.appRepo.GetOrCreateAppByKBIDAndType(ctx, kbID, domain.AppTypeWeb)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if webApp.Settings.StatsSetting.PVEnable {
|
||||
nodeStat, err := u.nodeRepo.GetNodeStatsByNodeId(ctx, nodeId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
node.PV = nodeStat.PV
|
||||
}
|
||||
}
|
||||
|
||||
if node.Meta.ContentType == domain.ContentTypeMD {
|
||||
return node, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -472,3 +472,28 @@ func (u *StatUseCase) AggregateHourlyStats(ctx context.Context) error {
|
|||
func (u *StatUseCase) CleanupOldHourlyStats(ctx context.Context) error {
|
||||
return u.repo.CleanupOldHourlyStats(ctx)
|
||||
}
|
||||
|
||||
// MigrateYesterdayPVToNodeStats 将昨天的PV数据从stat_page迁移到node_stats
|
||||
func (u *StatUseCase) MigrateYesterdayPVToNodeStats(ctx context.Context) error {
|
||||
// 获取昨天的PV数据,按node_id分组
|
||||
pvMap, err := u.repo.GetYesterdayPVByNode(ctx)
|
||||
if err != nil {
|
||||
u.logger.Error("failed to get yesterday PV data", log.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
// 遍历并插入/更新到node_stats表
|
||||
for nodeID, pvCount := range pvMap {
|
||||
if err := u.repo.UpsertNodeStats(ctx, nodeID, pvCount); err != nil {
|
||||
u.logger.Error("failed to upsert node stats",
|
||||
log.Error(err),
|
||||
log.String("node_id", nodeID),
|
||||
log.Int64("pv_count", pvCount))
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
u.logger.Info("successfully migrated yesterday PV data to node_stats",
|
||||
log.Int("node_count", len(pvMap)))
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ import Header from './Header';
|
|||
import Summary from './Summary';
|
||||
import Toc from './Toc';
|
||||
import Toolbar from './Toolbar';
|
||||
import IconPageview1 from '@panda-wiki/icons/IconPageview1';
|
||||
|
||||
interface WrapProps {
|
||||
detail: V1NodeDetailResp;
|
||||
|
|
@ -434,6 +435,15 @@ const Wrap = ({ detail: defaultDetail }: WrapProps) => {
|
|||
<IconZiti sx={{ fontSize: 12 }} />
|
||||
{characterCount} 字
|
||||
</Stack>
|
||||
<Stack
|
||||
direction={'row'}
|
||||
alignItems={'center'}
|
||||
gap={0.5}
|
||||
sx={{ fontSize: 12, color: 'text.tertiary' }}
|
||||
>
|
||||
<IconPageview1 sx={{ fontSize: 12 }} />
|
||||
浏览量 {nodeDetail?.pv}
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Box
|
||||
sx={{
|
||||
|
|
@ -681,19 +691,7 @@ const Wrap = ({ detail: defaultDetail }: WrapProps) => {
|
|||
</Box>
|
||||
<Box
|
||||
sx={{ ...(fixedToc && { display: 'flex' }) }}
|
||||
onKeyDown={event => {
|
||||
if ((event.ctrlKey || event.metaKey) && event.key === 's') {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
isMarkdown &&
|
||||
(event.ctrlKey || event.metaKey) &&
|
||||
event.key === 'b'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
event.stopPropagation();
|
||||
}}
|
||||
onKeyDown={event => event.stopPropagation()}
|
||||
>
|
||||
{isMarkdown ? (
|
||||
<Box
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import CardStyle from './CardStyle';
|
|||
import CardWebCustomCode from './CardWebCustomCode';
|
||||
import CardWebSEO from './CardWebSEO';
|
||||
import CardQaCopyright from './CardQaCopyright';
|
||||
import CardWebStats from './CardWebStats';
|
||||
|
||||
interface CardWebProps {
|
||||
kb: DomainKnowledgeBaseDetail;
|
||||
|
|
@ -134,6 +135,22 @@ const CardWeb = ({ kb, refresh }: CardWebProps) => {
|
|||
});
|
||||
}}
|
||||
/>
|
||||
<CardWebStats
|
||||
id={info.id}
|
||||
data={info}
|
||||
refresh={value => {
|
||||
setInfo({
|
||||
...info,
|
||||
settings: {
|
||||
...info.settings,
|
||||
stats_setting: {
|
||||
...info.settings?.stats_setting,
|
||||
...value,
|
||||
},
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,85 @@
|
|||
import { message } from '@ctzhian/ui';
|
||||
import { Box, FormControlLabel, Radio, RadioGroup } from '@mui/material';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import { DomainAppDetailResp } from '@/request/types';
|
||||
import { SettingCardItem, FormItem } from './Common';
|
||||
import { useAppSelector } from '@/store';
|
||||
import { putApiV1App } from '@/request/App';
|
||||
import { PROFESSION_VERSION_PERMISSION } from '@/constant/version.ts';
|
||||
import VersionMask from '@/components/VersionMask';
|
||||
|
||||
interface CardWebStatsProps {
|
||||
id: string;
|
||||
data: DomainAppDetailResp;
|
||||
refresh: (value: { pv_enable?: boolean }) => void;
|
||||
}
|
||||
|
||||
interface StatsFormData {
|
||||
pv_enable: 1 | 2;
|
||||
}
|
||||
|
||||
const CardWebStats = ({ data, id, refresh }: CardWebStatsProps) => {
|
||||
const [isEdit, setIsEdit] = useState(false);
|
||||
const { kb_id } = useAppSelector(state => state.config);
|
||||
const { handleSubmit, control, setValue } = useForm<StatsFormData>({
|
||||
defaultValues: {
|
||||
pv_enable: 2,
|
||||
},
|
||||
});
|
||||
|
||||
const onSubmit = handleSubmit((value: StatsFormData) => {
|
||||
const submitValue = {
|
||||
pv_enable: value.pv_enable === 1,
|
||||
};
|
||||
putApiV1App(
|
||||
{ id },
|
||||
{ kb_id, settings: { ...data.settings, stats_setting: submitValue } },
|
||||
).then(() => {
|
||||
message.success('保存成功');
|
||||
refresh(submitValue);
|
||||
setIsEdit(false);
|
||||
});
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const pvEnable = data.settings?.stats_setting?.pv_enable;
|
||||
setValue('pv_enable', pvEnable === true ? 1 : 2);
|
||||
}, [data]);
|
||||
|
||||
return (
|
||||
<SettingCardItem title='统计分析' isEdit={isEdit} onSubmit={onSubmit}>
|
||||
<VersionMask permission={PROFESSION_VERSION_PERMISSION}>
|
||||
<FormItem label='文档浏览量'>
|
||||
<Controller
|
||||
control={control}
|
||||
name='pv_enable'
|
||||
render={({ field }) => (
|
||||
<RadioGroup
|
||||
row
|
||||
{...field}
|
||||
onChange={e => {
|
||||
field.onChange(+e.target.value as 1 | 2);
|
||||
setIsEdit(true);
|
||||
}}
|
||||
>
|
||||
<FormControlLabel
|
||||
value={1}
|
||||
control={<Radio size='small' />}
|
||||
label={<Box sx={{ width: 100 }}>展示</Box>}
|
||||
/>
|
||||
<FormControlLabel
|
||||
value={2}
|
||||
control={<Radio size='small' />}
|
||||
label={<Box sx={{ width: 100 }}>隐藏</Box>}
|
||||
/>
|
||||
</RadioGroup>
|
||||
)}
|
||||
/>
|
||||
</FormItem>
|
||||
</VersionMask>
|
||||
</SettingCardItem>
|
||||
);
|
||||
};
|
||||
|
||||
export default CardWebStats;
|
||||
|
|
@ -341,6 +341,7 @@ export interface DomainAppSettings {
|
|||
recommend_node_ids?: string[];
|
||||
recommend_questions?: string[];
|
||||
search_placeholder?: string;
|
||||
stats_setting?: DomainStatsSetting;
|
||||
theme_and_style?: DomainThemeAndStyle;
|
||||
/** theme */
|
||||
theme_mode?: string;
|
||||
|
|
@ -428,6 +429,7 @@ export interface DomainAppSettingsResp {
|
|||
recommend_node_ids?: string[];
|
||||
recommend_questions?: string[];
|
||||
search_placeholder?: string;
|
||||
stats_setting?: DomainStatsSetting;
|
||||
theme_and_style?: DomainThemeAndStyle;
|
||||
/** theme */
|
||||
theme_mode?: string;
|
||||
|
|
@ -1256,6 +1258,10 @@ export interface DomainStatPageReq {
|
|||
scene: 1 | 2 | 3 | 4;
|
||||
}
|
||||
|
||||
export interface DomainStatsSetting {
|
||||
pv_enable?: boolean;
|
||||
}
|
||||
|
||||
export interface DomainSwitchModeReq {
|
||||
/** 百智云 API Key */
|
||||
auto_mode_api_key?: string;
|
||||
|
|
@ -1675,6 +1681,7 @@ export interface V1NodeDetailResp {
|
|||
permissions?: DomainNodePermissions;
|
||||
publisher_account?: string;
|
||||
publisher_id?: string;
|
||||
pv?: number;
|
||||
status?: DomainNodeStatus;
|
||||
type?: DomainNodeType;
|
||||
updated_at?: string;
|
||||
|
|
@ -1735,6 +1742,7 @@ export interface V1ShareNodeDetailResp {
|
|||
permissions?: DomainNodePermissions;
|
||||
publisher_account?: string;
|
||||
publisher_id?: string;
|
||||
pv?: number;
|
||||
status?: DomainNodeStatus;
|
||||
type?: DomainNodeType;
|
||||
updated_at?: string;
|
||||
|
|
|
|||
|
|
@ -341,6 +341,7 @@ export interface DomainAppSettings {
|
|||
recommend_node_ids?: string[];
|
||||
recommend_questions?: string[];
|
||||
search_placeholder?: string;
|
||||
stats_setting?: DomainStatsSetting;
|
||||
theme_and_style?: DomainThemeAndStyle;
|
||||
/** theme */
|
||||
theme_mode?: string;
|
||||
|
|
@ -428,6 +429,7 @@ export interface DomainAppSettingsResp {
|
|||
recommend_node_ids?: string[];
|
||||
recommend_questions?: string[];
|
||||
search_placeholder?: string;
|
||||
stats_setting?: DomainStatsSetting;
|
||||
theme_and_style?: DomainThemeAndStyle;
|
||||
/** theme */
|
||||
theme_mode?: string;
|
||||
|
|
@ -1256,6 +1258,10 @@ export interface DomainStatPageReq {
|
|||
scene: 1 | 2 | 3 | 4;
|
||||
}
|
||||
|
||||
export interface DomainStatsSetting {
|
||||
pv_enable?: boolean;
|
||||
}
|
||||
|
||||
export interface DomainSwitchModeReq {
|
||||
/** 百智云 API Key */
|
||||
auto_mode_api_key?: string;
|
||||
|
|
@ -1675,6 +1681,7 @@ export interface V1NodeDetailResp {
|
|||
permissions?: DomainNodePermissions;
|
||||
publisher_account?: string;
|
||||
publisher_id?: string;
|
||||
pv?: number;
|
||||
status?: DomainNodeStatus;
|
||||
type?: DomainNodeType;
|
||||
updated_at?: string;
|
||||
|
|
@ -1735,6 +1742,7 @@ export interface V1ShareNodeDetailResp {
|
|||
permissions?: DomainNodePermissions;
|
||||
publisher_account?: string;
|
||||
publisher_id?: string;
|
||||
pv?: number;
|
||||
status?: DomainNodeStatus;
|
||||
type?: DomainNodeType;
|
||||
updated_at?: string;
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import ConfirmModal from './ConfirmModal';
|
|||
import Header from './Header';
|
||||
import Toc from './Toc';
|
||||
import Toolbar from './Toolbar';
|
||||
import IconPageview1 from '@panda-wiki/icons/IconPageview1';
|
||||
|
||||
interface WrapProps {
|
||||
detail: V1NodeDetailResp;
|
||||
|
|
@ -295,6 +296,15 @@ const Wrap = ({ detail: defaultDetail = {} }: WrapProps) => {
|
|||
<IconZiti />
|
||||
{characterCount} 字
|
||||
</Stack>
|
||||
<Stack
|
||||
direction={'row'}
|
||||
alignItems={'center'}
|
||||
gap={0.5}
|
||||
sx={{ fontSize: 12, color: 'text.tertiary' }}
|
||||
>
|
||||
<IconPageview1 sx={{ fontSize: 12 }} />
|
||||
浏览量 {nodeDetail?.pv}
|
||||
</Stack>
|
||||
</Stack>
|
||||
{editorRef.editor && (
|
||||
<Box sx={{ ...(fixedToc && { display: 'flex' }) }}>
|
||||
|
|
|
|||
|
|
@ -236,6 +236,12 @@ const DocContent = ({
|
|||
<Box>{characterCount} 字</Box>
|
||||
</>
|
||||
)}
|
||||
{(info.pv ?? 0) > 0 && (
|
||||
<>
|
||||
<Box>·</Box>
|
||||
<Box>浏览量 {info.pv}</Box>
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
{info?.meta?.summary && (
|
||||
<Box
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
import React from 'react';
|
||||
import SvgIcon, { SvgIconProps } from '@mui/material/SvgIcon';
|
||||
|
||||
const IconPageview1 = (props: SvgIconProps) => (
|
||||
<SvgIcon
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
viewBox='0 0 1024 1024'
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d='M512 192c206.752 0 419.872 254.016 446.496 320-26.56 66.016-239.744 320-446.496 320-207.456 0-420.416-254.176-446.496-320 26.24-65.792 239.2-320 446.496-320zM512 128c-255.808 0-512 319.808-512 384 0 64.064 255.104 384 512 384 256.32 0 512-320.384 512-384 0-64-256.864-384-512-384l0 0z'
|
||||
fill='#444444'
|
||||
></path>
|
||||
<path
|
||||
d='M512 352c-88.416 0-160 71.648-160 160 0 88.384 71.584 160 160 160 88.448 0 160-71.616 160-160 0-88.352-71.552-160-160-160zM512 608c-52.992 0-96-43.008-96-96 0-53.024 43.008-96 96-96s96 42.976 96 96c0 52.992-42.944 96-96 96z'
|
||||
fill='#444444'
|
||||
></path>
|
||||
</SvgIcon>
|
||||
);
|
||||
|
||||
IconPageview1.displayName = 'icon-pageview1';
|
||||
|
||||
export default IconPageview1;
|
||||
|
|
@ -153,6 +153,7 @@ export { default as IconOllama } from './IconOllama';
|
|||
export { default as IconOpenrouter } from './IconOpenrouter';
|
||||
export { default as IconPCduan } from './IconPCduan';
|
||||
export { default as IconPDF } from './IconPDF';
|
||||
export { default as IconPageview1 } from './IconPageview1';
|
||||
export { default as IconPaperFull } from './IconPaperFull';
|
||||
export { default as IconPaperPlaneFill } from './IconPaperPlaneFill';
|
||||
export { default as IconPeizhi } from './IconPeizhi';
|
||||
|
|
|
|||
Loading…
Reference in New Issue