mirror of
https://github.com/ctwj/urldb.git
synced 2025-11-25 11:29:37 +08:00
add: 首页添加公告和右下角浮动按钮
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
package converter
|
package converter
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -90,6 +91,25 @@ func SystemConfigToResponse(configs []entity.SystemConfig) *dto.SystemConfigResp
|
|||||||
response.MeilisearchMasterKey = config.Value
|
response.MeilisearchMasterKey = config.Value
|
||||||
case entity.ConfigKeyMeilisearchIndexName:
|
case entity.ConfigKeyMeilisearchIndexName:
|
||||||
response.MeilisearchIndexName = config.Value
|
response.MeilisearchIndexName = config.Value
|
||||||
|
case entity.ConfigKeyEnableAnnouncements:
|
||||||
|
if val, err := strconv.ParseBool(config.Value); err == nil {
|
||||||
|
response.EnableAnnouncements = val
|
||||||
|
}
|
||||||
|
case entity.ConfigKeyAnnouncements:
|
||||||
|
if config.Value == "" || config.Value == "[]" {
|
||||||
|
response.Announcements = ""
|
||||||
|
} else {
|
||||||
|
// 在响应时保持为字符串,后续由前端处理
|
||||||
|
response.Announcements = config.Value
|
||||||
|
}
|
||||||
|
case entity.ConfigKeyEnableFloatButtons:
|
||||||
|
if val, err := strconv.ParseBool(config.Value); err == nil {
|
||||||
|
response.EnableFloatButtons = val
|
||||||
|
}
|
||||||
|
case entity.ConfigKeyWechatSearchImage:
|
||||||
|
response.WechatSearchImage = config.Value
|
||||||
|
case entity.ConfigKeyTelegramQrImage:
|
||||||
|
response.TelegramQrImage = config.Value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -221,6 +241,31 @@ func RequestToSystemConfig(req *dto.SystemConfigRequest) []entity.SystemConfig {
|
|||||||
updatedKeys = append(updatedKeys, entity.ConfigKeyMeilisearchIndexName)
|
updatedKeys = append(updatedKeys, entity.ConfigKeyMeilisearchIndexName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 界面配置处理
|
||||||
|
if req.EnableAnnouncements != nil {
|
||||||
|
configs = append(configs, entity.SystemConfig{Key: entity.ConfigKeyEnableAnnouncements, Value: strconv.FormatBool(*req.EnableAnnouncements), Type: entity.ConfigTypeBool})
|
||||||
|
updatedKeys = append(updatedKeys, entity.ConfigKeyEnableAnnouncements)
|
||||||
|
}
|
||||||
|
if req.Announcements != nil {
|
||||||
|
// 将数组转换为JSON字符串
|
||||||
|
if jsonBytes, err := json.Marshal(*req.Announcements); err == nil {
|
||||||
|
configs = append(configs, entity.SystemConfig{Key: entity.ConfigKeyAnnouncements, Value: string(jsonBytes), Type: entity.ConfigTypeJSON})
|
||||||
|
updatedKeys = append(updatedKeys, entity.ConfigKeyAnnouncements)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if req.EnableFloatButtons != nil {
|
||||||
|
configs = append(configs, entity.SystemConfig{Key: entity.ConfigKeyEnableFloatButtons, Value: strconv.FormatBool(*req.EnableFloatButtons), Type: entity.ConfigTypeBool})
|
||||||
|
updatedKeys = append(updatedKeys, entity.ConfigKeyEnableFloatButtons)
|
||||||
|
}
|
||||||
|
if req.WechatSearchImage != nil {
|
||||||
|
configs = append(configs, entity.SystemConfig{Key: entity.ConfigKeyWechatSearchImage, Value: *req.WechatSearchImage, Type: entity.ConfigTypeString})
|
||||||
|
updatedKeys = append(updatedKeys, entity.ConfigKeyWechatSearchImage)
|
||||||
|
}
|
||||||
|
if req.TelegramQrImage != nil {
|
||||||
|
configs = append(configs, entity.SystemConfig{Key: entity.ConfigKeyTelegramQrImage, Value: *req.TelegramQrImage, Type: entity.ConfigTypeString})
|
||||||
|
updatedKeys = append(updatedKeys, entity.ConfigKeyTelegramQrImage)
|
||||||
|
}
|
||||||
|
|
||||||
// 记录更新的配置项
|
// 记录更新的配置项
|
||||||
if len(updatedKeys) > 0 {
|
if len(updatedKeys) > 0 {
|
||||||
utils.Info("配置更新 - 被修改的配置项: %v", updatedKeys)
|
utils.Info("配置更新 - 被修改的配置项: %v", updatedKeys)
|
||||||
@@ -332,6 +377,24 @@ func SystemConfigToPublicResponse(configs []entity.SystemConfig) map[string]inte
|
|||||||
response[entity.ConfigResponseFieldMeilisearchMasterKey] = config.Value
|
response[entity.ConfigResponseFieldMeilisearchMasterKey] = config.Value
|
||||||
case entity.ConfigKeyMeilisearchIndexName:
|
case entity.ConfigKeyMeilisearchIndexName:
|
||||||
response[entity.ConfigResponseFieldMeilisearchIndexName] = config.Value
|
response[entity.ConfigResponseFieldMeilisearchIndexName] = config.Value
|
||||||
|
case entity.ConfigKeyEnableAnnouncements:
|
||||||
|
if val, err := strconv.ParseBool(config.Value); err == nil {
|
||||||
|
response["enable_announcements"] = val
|
||||||
|
}
|
||||||
|
case entity.ConfigKeyAnnouncements:
|
||||||
|
if config.Value == "" || config.Value == "[]" {
|
||||||
|
response["announcements"] = ""
|
||||||
|
} else {
|
||||||
|
response["announcements"] = config.Value
|
||||||
|
}
|
||||||
|
case entity.ConfigKeyEnableFloatButtons:
|
||||||
|
if val, err := strconv.ParseBool(config.Value); err == nil {
|
||||||
|
response["enable_float_buttons"] = val
|
||||||
|
}
|
||||||
|
case entity.ConfigKeyWechatSearchImage:
|
||||||
|
response["wechat_search_image"] = config.Value
|
||||||
|
case entity.ConfigKeyTelegramQrImage:
|
||||||
|
response["telegram_qr_image"] = config.Value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -372,5 +435,10 @@ func getDefaultConfigResponse() *dto.SystemConfigResponse {
|
|||||||
MeilisearchPort: entity.ConfigDefaultMeilisearchPort,
|
MeilisearchPort: entity.ConfigDefaultMeilisearchPort,
|
||||||
MeilisearchMasterKey: entity.ConfigDefaultMeilisearchMasterKey,
|
MeilisearchMasterKey: entity.ConfigDefaultMeilisearchMasterKey,
|
||||||
MeilisearchIndexName: entity.ConfigDefaultMeilisearchIndexName,
|
MeilisearchIndexName: entity.ConfigDefaultMeilisearchIndexName,
|
||||||
|
EnableAnnouncements: false,
|
||||||
|
Announcements: "",
|
||||||
|
EnableFloatButtons: false,
|
||||||
|
WechatSearchImage: entity.ConfigDefaultWechatSearchImage,
|
||||||
|
TelegramQrImage: entity.ConfigDefaultTelegramQrImage,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,6 +42,13 @@ type SystemConfigRequest struct {
|
|||||||
MeilisearchPort *string `json:"meilisearch_port,omitempty"`
|
MeilisearchPort *string `json:"meilisearch_port,omitempty"`
|
||||||
MeilisearchMasterKey *string `json:"meilisearch_master_key,omitempty"`
|
MeilisearchMasterKey *string `json:"meilisearch_master_key,omitempty"`
|
||||||
MeilisearchIndexName *string `json:"meilisearch_index_name,omitempty"`
|
MeilisearchIndexName *string `json:"meilisearch_index_name,omitempty"`
|
||||||
|
|
||||||
|
// 界面配置
|
||||||
|
EnableAnnouncements *bool `json:"enable_announcements,omitempty"`
|
||||||
|
Announcements *[]map[string]interface{} `json:"announcements,omitempty"`
|
||||||
|
EnableFloatButtons *bool `json:"enable_float_buttons,omitempty"`
|
||||||
|
WechatSearchImage *string `json:"wechat_search_image,omitempty"`
|
||||||
|
TelegramQrImage *string `json:"telegram_qr_image,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// SystemConfigResponse 系统配置响应
|
// SystemConfigResponse 系统配置响应
|
||||||
@@ -90,6 +97,13 @@ type SystemConfigResponse struct {
|
|||||||
MeilisearchPort string `json:"meilisearch_port"`
|
MeilisearchPort string `json:"meilisearch_port"`
|
||||||
MeilisearchMasterKey string `json:"meilisearch_master_key"`
|
MeilisearchMasterKey string `json:"meilisearch_master_key"`
|
||||||
MeilisearchIndexName string `json:"meilisearch_index_name"`
|
MeilisearchIndexName string `json:"meilisearch_index_name"`
|
||||||
|
|
||||||
|
// 界面配置
|
||||||
|
EnableAnnouncements bool `json:"enable_announcements"`
|
||||||
|
Announcements string `json:"announcements"`
|
||||||
|
EnableFloatButtons bool `json:"enable_float_buttons"`
|
||||||
|
WechatSearchImage string `json:"wechat_search_image"`
|
||||||
|
TelegramQrImage string `json:"telegram_qr_image"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// SystemConfigItem 单个配置项
|
// SystemConfigItem 单个配置项
|
||||||
|
|||||||
@@ -56,6 +56,13 @@ const (
|
|||||||
ConfigKeyTelegramProxyPort = "telegram_proxy_port"
|
ConfigKeyTelegramProxyPort = "telegram_proxy_port"
|
||||||
ConfigKeyTelegramProxyUsername = "telegram_proxy_username"
|
ConfigKeyTelegramProxyUsername = "telegram_proxy_username"
|
||||||
ConfigKeyTelegramProxyPassword = "telegram_proxy_password"
|
ConfigKeyTelegramProxyPassword = "telegram_proxy_password"
|
||||||
|
|
||||||
|
// 界面配置
|
||||||
|
ConfigKeyEnableAnnouncements = "enable_announcements"
|
||||||
|
ConfigKeyAnnouncements = "announcements"
|
||||||
|
ConfigKeyEnableFloatButtons = "enable_float_buttons"
|
||||||
|
ConfigKeyWechatSearchImage = "wechat_search_image"
|
||||||
|
ConfigKeyTelegramQrImage = "telegram_qr_image"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ConfigType 配置类型常量
|
// ConfigType 配置类型常量
|
||||||
@@ -183,4 +190,11 @@ const (
|
|||||||
ConfigDefaultTelegramProxyPort = "8080"
|
ConfigDefaultTelegramProxyPort = "8080"
|
||||||
ConfigDefaultTelegramProxyUsername = ""
|
ConfigDefaultTelegramProxyUsername = ""
|
||||||
ConfigDefaultTelegramProxyPassword = ""
|
ConfigDefaultTelegramProxyPassword = ""
|
||||||
|
|
||||||
|
// 界面配置默认值
|
||||||
|
ConfigDefaultEnableAnnouncements = "false"
|
||||||
|
ConfigDefaultAnnouncements = ""
|
||||||
|
ConfigDefaultEnableFloatButtons = "false"
|
||||||
|
ConfigDefaultWechatSearchImage = ""
|
||||||
|
ConfigDefaultTelegramQrImage = ""
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -133,6 +133,11 @@ func (r *SystemConfigRepositoryImpl) GetOrCreateDefault() ([]entity.SystemConfig
|
|||||||
{Key: entity.ConfigKeyMeilisearchPort, Value: entity.ConfigDefaultMeilisearchPort, Type: entity.ConfigTypeString},
|
{Key: entity.ConfigKeyMeilisearchPort, Value: entity.ConfigDefaultMeilisearchPort, Type: entity.ConfigTypeString},
|
||||||
{Key: entity.ConfigKeyMeilisearchMasterKey, Value: entity.ConfigDefaultMeilisearchMasterKey, Type: entity.ConfigTypeString},
|
{Key: entity.ConfigKeyMeilisearchMasterKey, Value: entity.ConfigDefaultMeilisearchMasterKey, Type: entity.ConfigTypeString},
|
||||||
{Key: entity.ConfigKeyMeilisearchIndexName, Value: entity.ConfigDefaultMeilisearchIndexName, Type: entity.ConfigTypeString},
|
{Key: entity.ConfigKeyMeilisearchIndexName, Value: entity.ConfigDefaultMeilisearchIndexName, Type: entity.ConfigTypeString},
|
||||||
|
{Key: entity.ConfigKeyEnableAnnouncements, Value: entity.ConfigDefaultEnableAnnouncements, Type: entity.ConfigTypeBool},
|
||||||
|
{Key: entity.ConfigKeyAnnouncements, Value: entity.ConfigDefaultAnnouncements, Type: entity.ConfigTypeJSON},
|
||||||
|
{Key: entity.ConfigKeyEnableFloatButtons, Value: entity.ConfigDefaultEnableFloatButtons, Type: entity.ConfigTypeBool},
|
||||||
|
{Key: entity.ConfigKeyWechatSearchImage, Value: entity.ConfigDefaultWechatSearchImage, Type: entity.ConfigTypeString},
|
||||||
|
{Key: entity.ConfigKeyTelegramQrImage, Value: entity.ConfigDefaultTelegramQrImage, Type: entity.ConfigTypeString},
|
||||||
}
|
}
|
||||||
|
|
||||||
err = r.UpsertConfigs(defaultConfigs)
|
err = r.UpsertConfigs(defaultConfigs)
|
||||||
@@ -169,6 +174,11 @@ func (r *SystemConfigRepositoryImpl) GetOrCreateDefault() ([]entity.SystemConfig
|
|||||||
entity.ConfigKeyMeilisearchPort: {Key: entity.ConfigKeyMeilisearchPort, Value: entity.ConfigDefaultMeilisearchPort, Type: entity.ConfigTypeString},
|
entity.ConfigKeyMeilisearchPort: {Key: entity.ConfigKeyMeilisearchPort, Value: entity.ConfigDefaultMeilisearchPort, Type: entity.ConfigTypeString},
|
||||||
entity.ConfigKeyMeilisearchMasterKey: {Key: entity.ConfigKeyMeilisearchMasterKey, Value: entity.ConfigDefaultMeilisearchMasterKey, Type: entity.ConfigTypeString},
|
entity.ConfigKeyMeilisearchMasterKey: {Key: entity.ConfigKeyMeilisearchMasterKey, Value: entity.ConfigDefaultMeilisearchMasterKey, Type: entity.ConfigTypeString},
|
||||||
entity.ConfigKeyMeilisearchIndexName: {Key: entity.ConfigKeyMeilisearchIndexName, Value: entity.ConfigDefaultMeilisearchIndexName, Type: entity.ConfigTypeString},
|
entity.ConfigKeyMeilisearchIndexName: {Key: entity.ConfigKeyMeilisearchIndexName, Value: entity.ConfigDefaultMeilisearchIndexName, Type: entity.ConfigTypeString},
|
||||||
|
entity.ConfigKeyEnableAnnouncements: {Key: entity.ConfigKeyEnableAnnouncements, Value: entity.ConfigDefaultEnableAnnouncements, Type: entity.ConfigTypeBool},
|
||||||
|
entity.ConfigKeyAnnouncements: {Key: entity.ConfigKeyAnnouncements, Value: entity.ConfigDefaultAnnouncements, Type: entity.ConfigTypeJSON},
|
||||||
|
entity.ConfigKeyEnableFloatButtons: {Key: entity.ConfigKeyEnableFloatButtons, Value: entity.ConfigDefaultEnableFloatButtons, Type: entity.ConfigTypeBool},
|
||||||
|
entity.ConfigKeyWechatSearchImage: {Key: entity.ConfigKeyWechatSearchImage, Value: entity.ConfigDefaultWechatSearchImage, Type: entity.ConfigTypeString},
|
||||||
|
entity.ConfigKeyTelegramQrImage: {Key: entity.ConfigKeyTelegramQrImage, Value: entity.ConfigDefaultTelegramQrImage, Type: entity.ConfigTypeString},
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查现有配置中是否有缺失的配置项
|
// 检查现有配置中是否有缺失的配置项
|
||||||
|
|||||||
@@ -125,6 +125,7 @@ func GetSystemConfig(c *gin.Context) {
|
|||||||
func UpdateSystemConfig(c *gin.Context) {
|
func UpdateSystemConfig(c *gin.Context) {
|
||||||
var req dto.SystemConfigRequest
|
var req dto.SystemConfigRequest
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
utils.Error("JSON绑定失败: %v", err)
|
||||||
ErrorResponse(c, "请求参数错误", http.StatusBadRequest)
|
ErrorResponse(c, "请求参数错误", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -141,30 +142,52 @@ func UpdateSystemConfig(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 验证参数 - 只验证提交的字段
|
// 验证参数 - 只验证提交的字段
|
||||||
if req.SiteTitle != nil && (len(*req.SiteTitle) < 1 || len(*req.SiteTitle) > 100) {
|
utils.Info("开始验证参数")
|
||||||
ErrorResponse(c, "网站标题长度必须在1-100字符之间", http.StatusBadRequest)
|
if req.SiteTitle != nil {
|
||||||
return
|
utils.Info("验证SiteTitle: '%s', 长度: %d", *req.SiteTitle, len(*req.SiteTitle))
|
||||||
|
if len(*req.SiteTitle) < 1 || len(*req.SiteTitle) > 100 {
|
||||||
|
ErrorResponse(c, "网站标题长度必须在1-100字符之间", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if req.AutoProcessInterval != nil && (*req.AutoProcessInterval < 1 || *req.AutoProcessInterval > 1440) {
|
if req.AutoProcessInterval != nil {
|
||||||
ErrorResponse(c, "自动处理间隔必须在1-1440分钟之间", http.StatusBadRequest)
|
utils.Info("验证AutoProcessInterval: %d", *req.AutoProcessInterval)
|
||||||
return
|
if *req.AutoProcessInterval < 1 || *req.AutoProcessInterval > 1440 {
|
||||||
|
ErrorResponse(c, "自动处理间隔必须在1-1440分钟之间", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if req.PageSize != nil && (*req.PageSize < 10 || *req.PageSize > 500) {
|
if req.PageSize != nil {
|
||||||
ErrorResponse(c, "每页显示数量必须在10-500之间", http.StatusBadRequest)
|
utils.Info("验证PageSize: %d", *req.PageSize)
|
||||||
return
|
if *req.PageSize < 10 || *req.PageSize > 500 {
|
||||||
|
ErrorResponse(c, "每页显示数量必须在10-500之间", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证自动转存配置
|
// 验证自动转存配置
|
||||||
if req.AutoTransferLimitDays != nil && (*req.AutoTransferLimitDays < 0 || *req.AutoTransferLimitDays > 365) {
|
if req.AutoTransferLimitDays != nil {
|
||||||
ErrorResponse(c, "自动转存限制天数必须在0-365之间", http.StatusBadRequest)
|
utils.Info("验证AutoTransferLimitDays: %d", *req.AutoTransferLimitDays)
|
||||||
return
|
if *req.AutoTransferLimitDays < 0 || *req.AutoTransferLimitDays > 365 {
|
||||||
|
ErrorResponse(c, "自动转存限制天数必须在0-365之间", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if req.AutoTransferMinSpace != nil && (*req.AutoTransferMinSpace < 100 || *req.AutoTransferMinSpace > 1024) {
|
if req.AutoTransferMinSpace != nil {
|
||||||
ErrorResponse(c, "最小存储空间必须在100-1024GB之间", http.StatusBadRequest)
|
utils.Info("验证AutoTransferMinSpace: %d", *req.AutoTransferMinSpace)
|
||||||
return
|
if *req.AutoTransferMinSpace < 100 || *req.AutoTransferMinSpace > 1024 {
|
||||||
|
ErrorResponse(c, "最小存储空间必须在100-1024GB之间", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证公告相关字段
|
||||||
|
if req.Announcements != nil {
|
||||||
|
utils.Info("验证Announcements: '%s'", *req.Announcements)
|
||||||
|
// 可以在这里添加更详细的验证逻辑
|
||||||
}
|
}
|
||||||
|
|
||||||
// 转换为实体
|
// 转换为实体
|
||||||
|
|||||||
1
web/components.d.ts
vendored
1
web/components.d.ts
vendored
@@ -34,6 +34,7 @@ declare module 'vue' {
|
|||||||
NInputNumber: typeof import('naive-ui')['NInputNumber']
|
NInputNumber: typeof import('naive-ui')['NInputNumber']
|
||||||
NList: typeof import('naive-ui')['NList']
|
NList: typeof import('naive-ui')['NList']
|
||||||
NListItem: typeof import('naive-ui')['NListItem']
|
NListItem: typeof import('naive-ui')['NListItem']
|
||||||
|
NMarquee: typeof import('naive-ui')['NMarquee']
|
||||||
NMessageProvider: typeof import('naive-ui')['NMessageProvider']
|
NMessageProvider: typeof import('naive-ui')['NMessageProvider']
|
||||||
NModal: typeof import('naive-ui')['NModal']
|
NModal: typeof import('naive-ui')['NModal']
|
||||||
NNotificationProvider: typeof import('naive-ui')['NNotificationProvider']
|
NNotificationProvider: typeof import('naive-ui')['NNotificationProvider']
|
||||||
|
|||||||
157
web/components/Admin/AnnouncementConfig.vue
Normal file
157
web/components/Admin/AnnouncementConfig.vue
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
<template>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<label class="text-lg font-semibold text-gray-800 dark:text-gray-200">公告配置</label>
|
||||||
|
<span class="text-xs text-gray-500 dark:text-gray-400">开启后可在网站显示公告信息</span>
|
||||||
|
</div>
|
||||||
|
<n-button v-if="modelValue.enable_announcements" @click="addAnnouncement" type="primary" size="small">
|
||||||
|
<template #icon>
|
||||||
|
<i class="fas fa-plus"></i>
|
||||||
|
</template>
|
||||||
|
添加公告
|
||||||
|
</n-button>
|
||||||
|
</div>
|
||||||
|
<n-switch v-model:value="enableAnnouncements" />
|
||||||
|
|
||||||
|
<!-- 公告列表 -->
|
||||||
|
<div v-if="modelValue.enable_announcements && modelValue.announcements && modelValue.announcements.length > 0" class="announcement-list space-y-3">
|
||||||
|
<div v-for="(announcement, index) in modelValue.announcements" :key="index" class="announcement-item border rounded-lg p-3 bg-gray-50 dark:bg-gray-800">
|
||||||
|
<div class="flex items-center justify-between mb-2">
|
||||||
|
<div class="flex items-center space-x-3">
|
||||||
|
<h4 class="text-sm font-medium text-gray-900 dark:text-gray-100">公告 {{ index + 1 }}</h4>
|
||||||
|
<n-switch v-model:value="announcement.enabled" size="small" />
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center space-x-1">
|
||||||
|
<n-button text @click="moveAnnouncementUp(index)" :disabled="index === 0" size="small">
|
||||||
|
<template #icon>
|
||||||
|
<i class="fas fa-arrow-up"></i>
|
||||||
|
</template>
|
||||||
|
</n-button>
|
||||||
|
<n-button text @click="moveAnnouncementDown(index)" :disabled="index === modelValue.announcements.length - 1" size="small">
|
||||||
|
<template #icon>
|
||||||
|
<i class="fas fa-arrow-down"></i>
|
||||||
|
</template>
|
||||||
|
</n-button>
|
||||||
|
<n-button text @click="removeAnnouncement(index)" type="error" size="small">
|
||||||
|
<template #icon>
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
</template>
|
||||||
|
</n-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-2">
|
||||||
|
<n-input
|
||||||
|
v-model:value="announcement.content"
|
||||||
|
placeholder="公告内容,支持HTML标签"
|
||||||
|
type="textarea"
|
||||||
|
:rows="2"
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
// 公告接口
|
||||||
|
interface Announcement {
|
||||||
|
content: string
|
||||||
|
enabled: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
// 配置数据接口
|
||||||
|
interface ConfigData {
|
||||||
|
enable_announcements: boolean
|
||||||
|
announcements: Announcement[]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Props
|
||||||
|
const props = defineProps<{
|
||||||
|
modelValue: ConfigData
|
||||||
|
}>()
|
||||||
|
|
||||||
|
// Emits
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'update:modelValue': [value: ConfigData]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
// 计算属性用于双向绑定
|
||||||
|
const enableAnnouncements = computed({
|
||||||
|
get: () => props.modelValue.enable_announcements,
|
||||||
|
set: (value: boolean) => {
|
||||||
|
emit('update:modelValue', {
|
||||||
|
enable_announcements: value,
|
||||||
|
announcements: props.modelValue.announcements
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log(props.modelValue)
|
||||||
|
|
||||||
|
// 更新数据
|
||||||
|
const updateValue = (newValue: ConfigData) => {
|
||||||
|
emit('update:modelValue', newValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加公告
|
||||||
|
const addAnnouncement = () => {
|
||||||
|
const newAnnouncements = [...props.modelValue.announcements, {
|
||||||
|
content: '',
|
||||||
|
enabled: true
|
||||||
|
}]
|
||||||
|
updateValue({
|
||||||
|
enable_announcements: props.modelValue.enable_announcements,
|
||||||
|
announcements: newAnnouncements
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除公告
|
||||||
|
const removeAnnouncement = (index: number) => {
|
||||||
|
const currentAnnouncements = Array.isArray(props.modelValue.announcements) ? props.modelValue.announcements : []
|
||||||
|
const newAnnouncements = currentAnnouncements.filter((_, i) => i !== index)
|
||||||
|
updateValue({
|
||||||
|
enable_announcements: props.modelValue.enable_announcements,
|
||||||
|
announcements: newAnnouncements
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 上移公告
|
||||||
|
const moveAnnouncementUp = (index: number) => {
|
||||||
|
if (index > 0) {
|
||||||
|
const currentAnnouncements = Array.isArray(props.modelValue.announcements) ? props.modelValue.announcements : []
|
||||||
|
const newAnnouncements = [...currentAnnouncements]
|
||||||
|
const temp = newAnnouncements[index]
|
||||||
|
newAnnouncements[index] = newAnnouncements[index - 1]
|
||||||
|
newAnnouncements[index - 1] = temp
|
||||||
|
updateValue({
|
||||||
|
enable_announcements: props.modelValue.enable_announcements,
|
||||||
|
announcements: newAnnouncements
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 下移公告
|
||||||
|
const moveAnnouncementDown = (index: number) => {
|
||||||
|
const currentAnnouncements = Array.isArray(props.modelValue.announcements) ? props.modelValue.announcements : []
|
||||||
|
if (index < currentAnnouncements.length - 1) {
|
||||||
|
const newAnnouncements = [...currentAnnouncements]
|
||||||
|
const temp = newAnnouncements[index]
|
||||||
|
newAnnouncements[index] = newAnnouncements[index + 1]
|
||||||
|
newAnnouncements[index + 1] = temp
|
||||||
|
updateValue({
|
||||||
|
enable_announcements: props.modelValue.enable_announcements,
|
||||||
|
announcements: newAnnouncements
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.announcement-list {
|
||||||
|
max-height: 400px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
171
web/components/Admin/FloatButtonsConfig.vue
Normal file
171
web/components/Admin/FloatButtonsConfig.vue
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
<template>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<label class="text-lg font-semibold text-gray-800 dark:text-gray-200">浮动按钮配置</label>
|
||||||
|
<span class="text-xs text-gray-500 dark:text-gray-400">开启后显示右下角浮动按钮</span>
|
||||||
|
</div>
|
||||||
|
<n-switch v-model:value="enableFloatButtons" />
|
||||||
|
|
||||||
|
<!-- 浮动按钮设置 -->
|
||||||
|
<div v-if="modelValue.enable_float_buttons" class="float-buttons-config space-y-4">
|
||||||
|
<!-- 微信搜一搜图片 -->
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<label class="text-base font-semibold text-gray-800 dark:text-gray-200">微信搜一搜图片</label>
|
||||||
|
<span class="text-xs text-gray-500 dark:text-gray-400">选择微信搜一搜的二维码图片</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center space-x-4">
|
||||||
|
<div v-if="modelValue.wechat_search_image" class="flex-shrink-0">
|
||||||
|
<n-image
|
||||||
|
:src="getImageUrl(modelValue.wechat_search_image)"
|
||||||
|
alt="微信搜一搜"
|
||||||
|
width="80"
|
||||||
|
height="80"
|
||||||
|
object-fit="cover"
|
||||||
|
class="rounded-lg border"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1">
|
||||||
|
<n-button type="primary" @click="openWechatSelector">
|
||||||
|
<template #icon>
|
||||||
|
<i class="fas fa-image"></i>
|
||||||
|
</template>
|
||||||
|
{{ modelValue.wechat_search_image ? '更换图片' : '选择图片' }}
|
||||||
|
</n-button>
|
||||||
|
<n-button v-if="modelValue.wechat_search_image" @click="clearWechatImage" class="ml-2">
|
||||||
|
<template #icon>
|
||||||
|
<i class="fas fa-times"></i>
|
||||||
|
</template>
|
||||||
|
清除
|
||||||
|
</n-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Telegram二维码 -->
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<label class="text-base font-semibold text-gray-800 dark:text-gray-200">Telegram二维码</label>
|
||||||
|
<span class="text-xs text-gray-500 dark:text-gray-400">选择Telegram群组的二维码图片</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center space-x-4">
|
||||||
|
<div v-if="modelValue.telegram_qr_image" class="flex-shrink-0">
|
||||||
|
<n-image
|
||||||
|
:src="getImageUrl(modelValue.telegram_qr_image)"
|
||||||
|
alt="Telegram二维码"
|
||||||
|
width="80"
|
||||||
|
height="80"
|
||||||
|
object-fit="cover"
|
||||||
|
class="rounded-lg border"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1">
|
||||||
|
<n-button type="primary" @click="openTelegramSelector">
|
||||||
|
<template #icon>
|
||||||
|
<i class="fas fa-image"></i>
|
||||||
|
</template>
|
||||||
|
{{ modelValue.telegram_qr_image ? '更换图片' : '选择图片' }}
|
||||||
|
</n-button>
|
||||||
|
<n-button v-if="modelValue.telegram_qr_image" @click="clearTelegramImage" class="ml-2">
|
||||||
|
<template #icon>
|
||||||
|
<i class="fas fa-times"></i>
|
||||||
|
</template>
|
||||||
|
清除
|
||||||
|
</n-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
// 使用图片URL composable
|
||||||
|
const { getImageUrl } = useImageUrl()
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import ImageSelectorModal from '~/components/Admin/ImageSelectorModal.vue'
|
||||||
|
// 配置数据接口
|
||||||
|
interface ConfigData {
|
||||||
|
enable_float_buttons: boolean
|
||||||
|
wechat_search_image: string
|
||||||
|
telegram_qr_image: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Props
|
||||||
|
const props = defineProps<{
|
||||||
|
modelValue: ConfigData
|
||||||
|
}>()
|
||||||
|
|
||||||
|
// Emits
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'update:modelValue': [value: ConfigData]
|
||||||
|
'openWechatSelector': []
|
||||||
|
'openTelegramSelector': []
|
||||||
|
}>()
|
||||||
|
|
||||||
|
// 计算属性用于双向绑定
|
||||||
|
const enableFloatButtons = computed({
|
||||||
|
get: () => props.modelValue.enable_float_buttons,
|
||||||
|
set: (value: boolean) => {
|
||||||
|
emit('update:modelValue', {
|
||||||
|
enable_float_buttons: value,
|
||||||
|
wechat_search_image: props.modelValue.wechat_search_image,
|
||||||
|
telegram_qr_image: props.modelValue.telegram_qr_image
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 使用图片URL composable
|
||||||
|
const { getImageUrl } = useImageUrl()
|
||||||
|
|
||||||
|
// 选择器状态
|
||||||
|
const showWechatSelector = ref(false)
|
||||||
|
const showTelegramSelector = ref(false)
|
||||||
|
|
||||||
|
// 清除微信图片
|
||||||
|
const clearWechatImage = () => {
|
||||||
|
emit('update:modelValue', {
|
||||||
|
enable_float_buttons: props.modelValue.enable_float_buttons,
|
||||||
|
wechat_search_image: '',
|
||||||
|
telegram_qr_image: props.modelValue.telegram_qr_image
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清除Telegram图片
|
||||||
|
const clearTelegramImage = () => {
|
||||||
|
emit('update:modelValue', {
|
||||||
|
enable_float_buttons: props.modelValue.enable_float_buttons,
|
||||||
|
wechat_search_image: props.modelValue.wechat_search_image,
|
||||||
|
telegram_qr_image: ''
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 打开微信选择器
|
||||||
|
const openWechatSelector = () => {
|
||||||
|
emit('openWechatSelector')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 打开Telegram选择器
|
||||||
|
const openTelegramSelector = () => {
|
||||||
|
emit('openTelegramSelector')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理微信图片选择
|
||||||
|
const handleWechatImageSelect = (file: any) => {
|
||||||
|
emit('update:modelValue', {
|
||||||
|
enable_float_buttons: props.modelValue.enable_float_buttons,
|
||||||
|
wechat_search_image: file.access_url,
|
||||||
|
telegram_qr_image: props.modelValue.telegram_qr_image
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理Telegram图片选择
|
||||||
|
const handleTelegramImageSelect = (file: any) => {
|
||||||
|
emit('update:modelValue', {
|
||||||
|
enable_float_buttons: props.modelValue.enable_float_buttons,
|
||||||
|
wechat_search_image: props.modelValue.wechat_search_image,
|
||||||
|
telegram_qr_image: file.access_url
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
256
web/components/Admin/ImageSelectorModal.vue
Normal file
256
web/components/Admin/ImageSelectorModal.vue
Normal file
@@ -0,0 +1,256 @@
|
|||||||
|
<template>
|
||||||
|
<n-modal v-model:show="showModal" preset="card" :title="title" style="width: 90vw; max-width: 1200px; max-height: 80vh;">
|
||||||
|
<div class="space-y-4">
|
||||||
|
<!-- 搜索 -->
|
||||||
|
<div class="flex gap-4">
|
||||||
|
<n-input
|
||||||
|
v-model:value="searchKeyword"
|
||||||
|
placeholder="搜索文件名..."
|
||||||
|
@keyup.enter="handleSearch"
|
||||||
|
class="flex-1"
|
||||||
|
clearable
|
||||||
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<i class="fas fa-search"></i>
|
||||||
|
</template>
|
||||||
|
</n-input>
|
||||||
|
|
||||||
|
<n-button type="primary" @click="handleSearch" class="w-20">
|
||||||
|
<template #icon>
|
||||||
|
<i class="fas fa-search"></i>
|
||||||
|
</template>
|
||||||
|
搜索
|
||||||
|
</n-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 文件列表 -->
|
||||||
|
<div v-if="loading" class="flex items-center justify-center py-8">
|
||||||
|
<n-spin size="large" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="fileList.length === 0" class="text-center py-8">
|
||||||
|
<i class="fas fa-file-upload text-4xl text-gray-400 mb-4"></i>
|
||||||
|
<p class="text-gray-500">暂无图片文件</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="file-grid">
|
||||||
|
<div
|
||||||
|
v-for="file in fileList"
|
||||||
|
:key="file.id"
|
||||||
|
class="file-item cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-lg p-3 transition-colors"
|
||||||
|
:class="{ 'bg-blue-50 dark:bg-blue-900/20 border-2 border-blue-300 dark:border-blue-600': selectedFileId === file.id }"
|
||||||
|
@click="selectFile(file)"
|
||||||
|
>
|
||||||
|
<div class="image-preview">
|
||||||
|
<n-image
|
||||||
|
:src="getImageUrl(file.access_url)"
|
||||||
|
:alt="file.original_name"
|
||||||
|
:lazy="false"
|
||||||
|
object-fit="cover"
|
||||||
|
class="preview-image rounded"
|
||||||
|
@error="handleImageError"
|
||||||
|
@load="handleImageLoad"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="image-info mt-2">
|
||||||
|
<div class="file-name text-sm font-medium text-gray-900 dark:text-gray-100 truncate">
|
||||||
|
{{ file.original_name }}
|
||||||
|
</div>
|
||||||
|
<div class="file-size text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
{{ formatFileSize(file.file_size) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 分页 -->
|
||||||
|
<div class="pagination-wrapper">
|
||||||
|
<n-pagination
|
||||||
|
v-model:page="pagination.page"
|
||||||
|
v-model:page-size="pagination.pageSize"
|
||||||
|
:page-count="Math.ceil(pagination.total / pagination.pageSize)"
|
||||||
|
:page-sizes="pagination.pageSizes"
|
||||||
|
show-size-picker
|
||||||
|
@update:page="handlePageChange"
|
||||||
|
@update:page-size="handlePageSizeChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<n-space justify="end">
|
||||||
|
<n-button @click="closeModal">取消</n-button>
|
||||||
|
<n-button
|
||||||
|
type="primary"
|
||||||
|
@click="confirmSelection"
|
||||||
|
:disabled="!selectedFileId"
|
||||||
|
>
|
||||||
|
确认选择
|
||||||
|
</n-button>
|
||||||
|
</n-space>
|
||||||
|
</template>
|
||||||
|
</n-modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
// Props
|
||||||
|
const props = defineProps<{
|
||||||
|
show: boolean
|
||||||
|
title: string
|
||||||
|
}>()
|
||||||
|
|
||||||
|
// Emits
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'update:show': [value: boolean]
|
||||||
|
'select': [file: any]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
// 使用图片URL composable
|
||||||
|
const { getImageUrl } = useImageUrl()
|
||||||
|
|
||||||
|
// 响应式数据
|
||||||
|
const showModal = computed({
|
||||||
|
get: () => props.show,
|
||||||
|
set: (value) => emit('update:show', value)
|
||||||
|
})
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
const fileList = ref<any[]>([])
|
||||||
|
const selectedFileId = ref<number | null>(null)
|
||||||
|
const searchKeyword = ref('')
|
||||||
|
|
||||||
|
const pagination = ref({
|
||||||
|
page: 1,
|
||||||
|
pageSize: 20,
|
||||||
|
total: 0,
|
||||||
|
pageSizes: [10, 20, 50, 100]
|
||||||
|
})
|
||||||
|
|
||||||
|
// 监听show变化,重新加载数据
|
||||||
|
watch(() => props.show, (newValue) => {
|
||||||
|
if (newValue) {
|
||||||
|
loadFileList()
|
||||||
|
} else {
|
||||||
|
// 重置状态
|
||||||
|
selectedFileId.value = null
|
||||||
|
searchKeyword.value = ''
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 加载文件列表
|
||||||
|
const loadFileList = async () => {
|
||||||
|
try {
|
||||||
|
loading.value = true
|
||||||
|
const { useFileApi } = await import('~/composables/useFileApi')
|
||||||
|
const fileApi = useFileApi()
|
||||||
|
|
||||||
|
const response = await fileApi.getFileList({
|
||||||
|
page: pagination.value.page,
|
||||||
|
pageSize: pagination.value.pageSize,
|
||||||
|
search: searchKeyword.value,
|
||||||
|
fileType: 'image',
|
||||||
|
status: 'active'
|
||||||
|
}) as any
|
||||||
|
|
||||||
|
if (response && response.data) {
|
||||||
|
fileList.value = response.data.files || []
|
||||||
|
pagination.value.total = response.data.total || 0
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取文件列表失败:', error)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 搜索处理
|
||||||
|
const handleSearch = () => {
|
||||||
|
pagination.value.page = 1
|
||||||
|
loadFileList()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分页处理
|
||||||
|
const handlePageChange = (page: number) => {
|
||||||
|
pagination.value.page = page
|
||||||
|
loadFileList()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePageSizeChange = (pageSize: number) => {
|
||||||
|
pagination.value.pageSize = pageSize
|
||||||
|
pagination.value.page = 1
|
||||||
|
loadFileList()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 文件选择
|
||||||
|
const selectFile = (file: any) => {
|
||||||
|
selectedFileId.value = file.id
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确认选择
|
||||||
|
const confirmSelection = () => {
|
||||||
|
if (selectedFileId.value) {
|
||||||
|
const file = fileList.value.find(f => f.id === selectedFileId.value)
|
||||||
|
if (file) {
|
||||||
|
emit('select', file)
|
||||||
|
closeModal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关闭模态框
|
||||||
|
const closeModal = () => {
|
||||||
|
showModal.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 文件大小格式化
|
||||||
|
const formatFileSize = (size: number) => {
|
||||||
|
if (size < 1024) return size + ' B'
|
||||||
|
if (size < 1024 * 1024) return (size / 1024).toFixed(1) + ' KB'
|
||||||
|
if (size < 1024 * 1024 * 1024) return (size / (1024 * 1024)).toFixed(1) + ' MB'
|
||||||
|
return (size / (1024 * 1024 * 1024)).toFixed(1) + ' GB'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 图片加载处理
|
||||||
|
const handleImageError = (event: any) => {
|
||||||
|
console.error('图片加载失败:', event)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleImageLoad = (event: any) => {
|
||||||
|
console.log('图片加载成功:', event)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.file-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||||
|
gap: 1rem;
|
||||||
|
max-height: 400px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-item {
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-item:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-image {
|
||||||
|
width: 100%;
|
||||||
|
height: 120px;
|
||||||
|
object-fit: cover;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-wrapper {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
118
web/components/Announcement.vue
Normal file
118
web/components/Announcement.vue
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
<template>
|
||||||
|
<div v-if="shouldShowAnnouncement" class="announcement-container px-3 py-1">
|
||||||
|
<div class="flex items-center justify-between min-h-[24px]">
|
||||||
|
<div class="flex items-center gap-2 flex-1 overflow-hidden">
|
||||||
|
<i class="fas fa-bullhorn text-blue-600 dark:text-blue-400 text-sm flex-shrink-0"></i>
|
||||||
|
<div class="announcement-content overflow-hidden">
|
||||||
|
<div class="announcement-item active">
|
||||||
|
<span class="text-sm text-gray-700 dark:text-gray-300 truncate">{{ validAnnouncements[currentIndex].content }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-1 text-xs text-gray-500 dark:text-gray-400 flex-shrink-0 ml-2">
|
||||||
|
<span>{{ (currentIndex + 1) }}/{{ validAnnouncements.length }}</span>
|
||||||
|
<button @click="nextAnnouncement" class="hover:text-blue-500 transition-colors">
|
||||||
|
<i class="fas fa-chevron-right text-xs"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
// 使用系统配置store获取公告数据
|
||||||
|
import { useSystemConfigStore } from '~/stores/systemConfig'
|
||||||
|
const systemConfigStore = useSystemConfigStore()
|
||||||
|
await systemConfigStore.initConfig(false, false)
|
||||||
|
const systemConfig = computed(() => systemConfigStore.config)
|
||||||
|
|
||||||
|
interface AnnouncementItem {
|
||||||
|
content: string
|
||||||
|
enabled: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentIndex = ref(0)
|
||||||
|
const interval = ref<NodeJS.Timeout | null>(null)
|
||||||
|
|
||||||
|
// 计算有效公告(开启状态且有内容的公告)
|
||||||
|
const validAnnouncements = computed(() => {
|
||||||
|
if (!systemConfig.value?.announcements) return []
|
||||||
|
|
||||||
|
const announcements = Array.isArray(systemConfig.value.announcements)
|
||||||
|
? systemConfig.value.announcements
|
||||||
|
: JSON.parse(systemConfig.value.announcements || '[]')
|
||||||
|
|
||||||
|
return announcements.filter((item: AnnouncementItem) =>
|
||||||
|
item.enabled && item.content && item.content.trim()
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 判断是否应该显示公告
|
||||||
|
const shouldShowAnnouncement = computed(() => {
|
||||||
|
return systemConfig.value?.enable_announcements && validAnnouncements.value.length > 0
|
||||||
|
})
|
||||||
|
|
||||||
|
// 自动切换公告
|
||||||
|
const startAutoSwitch = () => {
|
||||||
|
if (validAnnouncements.value.length > 1) {
|
||||||
|
interval.value = setInterval(() => {
|
||||||
|
currentIndex.value = (currentIndex.value + 1) % validAnnouncements.value.length
|
||||||
|
}, 4000) // 每4秒切换一次
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 手动切换到下一条公告
|
||||||
|
const nextAnnouncement = () => {
|
||||||
|
currentIndex.value = (currentIndex.value + 1) % validAnnouncements.value.length
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听公告数据变化,重新开始自动切换
|
||||||
|
watch(() => validAnnouncements.value.length, (newLength) => {
|
||||||
|
if (newLength > 0) {
|
||||||
|
currentIndex.value = 0
|
||||||
|
stopAutoSwitch()
|
||||||
|
startAutoSwitch()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 清理定时器
|
||||||
|
const stopAutoSwitch = () => {
|
||||||
|
if (interval.value) {
|
||||||
|
clearInterval(interval.value)
|
||||||
|
interval.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (shouldShowAnnouncement.value) {
|
||||||
|
startAutoSwitch()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
stopAutoSwitch()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.announcement-content {
|
||||||
|
position: relative;
|
||||||
|
height: 20px; /* 固定高度 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.announcement-item {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(5px);
|
||||||
|
transition: all 0.5s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.announcement-item.active {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 暗色主题适配 */
|
||||||
|
.dark-theme .announcement-container {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
"@pinia/nuxt": "^0.5.0",
|
"@pinia/nuxt": "^0.5.0",
|
||||||
"@vicons/ionicons5": "^0.12.0",
|
"@vicons/ionicons5": "^0.12.0",
|
||||||
"chart.js": "^4.5.0",
|
"chart.js": "^4.5.0",
|
||||||
"naive-ui": "^2.37.0",
|
"naive-ui": "^2.42.0",
|
||||||
"pinia": "^2.1.0",
|
"pinia": "^2.1.0",
|
||||||
"vfonts": "^0.0.3",
|
"vfonts": "^0.0.3",
|
||||||
"vue": "^3.3.0",
|
"vue": "^3.3.0",
|
||||||
|
|||||||
@@ -176,103 +176,59 @@
|
|||||||
</n-form>
|
</n-form>
|
||||||
</div>
|
</div>
|
||||||
</n-tab-pane>
|
</n-tab-pane>
|
||||||
|
|
||||||
|
<n-tab-pane name="ui" tab="界面配置">
|
||||||
|
<div class="tab-content-container">
|
||||||
|
<n-form
|
||||||
|
ref="formRef"
|
||||||
|
:model="configForm"
|
||||||
|
:rules="rules"
|
||||||
|
label-placement="left"
|
||||||
|
label-width="auto"
|
||||||
|
require-mark-placement="right-hanging"
|
||||||
|
>
|
||||||
|
<div class="space-y-6">
|
||||||
|
<!-- 公告配置组件 -->
|
||||||
|
<AnnouncementConfig
|
||||||
|
v-model="announcementConfig"
|
||||||
|
@update:modelValue="handleAnnouncementUpdate"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 浮动按钮配置组件 -->
|
||||||
|
<FloatButtonsConfig
|
||||||
|
v-model="floatButtonsConfig"
|
||||||
|
@update:modelValue="handleFloatButtonsUpdate"
|
||||||
|
@openWechatSelector="showWechatSelector = true"
|
||||||
|
@openTelegramSelector="showTelegramSelector = true"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 微信图片选择器 -->
|
||||||
|
<ImageSelectorModal
|
||||||
|
v-model:show="showWechatSelector"
|
||||||
|
title="选择微信搜一搜图片"
|
||||||
|
@select="handleWechatImageSelect"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Telegram图片选择器 -->
|
||||||
|
<ImageSelectorModal
|
||||||
|
v-model:show="showTelegramSelector"
|
||||||
|
title="选择Telegram二维码图片"
|
||||||
|
@select="handleTelegramImageSelect"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</n-form>
|
||||||
|
</div>
|
||||||
|
</n-tab-pane>
|
||||||
</n-tabs>
|
</n-tabs>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</AdminPageLayout>
|
</AdminPageLayout>
|
||||||
<!-- Logo选择模态框 -->
|
<!-- ImageSelectorModal 组件 -->
|
||||||
<n-modal v-model:show="showLogoSelector" preset="card" title="选择Logo图片" style="width: 90vw; max-width: 1200px; max-height: 80vh;">
|
<ImageSelectorModal
|
||||||
<div class="space-y-4">
|
v-model:show="showLogoSelector"
|
||||||
<!-- 搜索 -->
|
title="选择Logo图片"
|
||||||
<div class="flex gap-4">
|
@select="handleLogoSelect"
|
||||||
<n-input
|
/>
|
||||||
v-model:value="searchKeyword"
|
|
||||||
placeholder="搜索文件名..."
|
|
||||||
@keyup.enter="handleSearch"
|
|
||||||
class="flex-1"
|
|
||||||
clearable
|
|
||||||
>
|
|
||||||
<template #prefix>
|
|
||||||
<i class="fas fa-search"></i>
|
|
||||||
</template>
|
|
||||||
</n-input>
|
|
||||||
|
|
||||||
<n-button type="primary" @click="handleSearch" class="w-20">
|
|
||||||
<template #icon>
|
|
||||||
<i class="fas fa-search"></i>
|
|
||||||
</template>
|
|
||||||
搜索
|
|
||||||
</n-button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 文件列表 -->
|
|
||||||
<div v-if="loading" class="flex items-center justify-center py-8">
|
|
||||||
<n-spin size="large" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-else-if="fileList.length === 0" class="text-center py-8">
|
|
||||||
<i class="fas fa-file-upload text-4xl text-gray-400 mb-4"></i>
|
|
||||||
<p class="text-gray-500">暂无图片文件</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-else class="file-grid">
|
|
||||||
<div
|
|
||||||
v-for="file in fileList"
|
|
||||||
:key="file.id"
|
|
||||||
class="file-item cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-lg p-3 transition-colors"
|
|
||||||
:class="{ 'bg-blue-50 dark:bg-blue-900/20 border-2 border-blue-300 dark:border-blue-600': selectedFileId === file.id }"
|
|
||||||
@click="selectFile(file)"
|
|
||||||
>
|
|
||||||
<div class="image-preview">
|
|
||||||
<n-image
|
|
||||||
:src="getImageUrl(file.access_url)"
|
|
||||||
:alt="file.original_name"
|
|
||||||
:lazy="false"
|
|
||||||
object-fit="cover"
|
|
||||||
class="preview-image rounded"
|
|
||||||
@error="handleImageError"
|
|
||||||
@load="handleImageLoad"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div class="image-info mt-2">
|
|
||||||
<div class="file-name text-sm font-medium text-gray-900 dark:text-gray-100 truncate">
|
|
||||||
{{ file.original_name }}
|
|
||||||
</div>
|
|
||||||
<div class="file-size text-xs text-gray-500 dark:text-gray-400">
|
|
||||||
{{ formatFileSize(file.file_size) }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 分页 -->
|
|
||||||
<div class="pagination-wrapper">
|
|
||||||
<n-pagination
|
|
||||||
v-model:page="pagination.page"
|
|
||||||
v-model:page-size="pagination.pageSize"
|
|
||||||
:page-count="Math.ceil(pagination.total / pagination.pageSize)"
|
|
||||||
:page-sizes="pagination.pageSizes"
|
|
||||||
show-size-picker
|
|
||||||
@update:page="handlePageChange"
|
|
||||||
@update:page-size="handlePageSizeChange"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<template #footer>
|
|
||||||
<n-space justify="end">
|
|
||||||
<n-button @click="showLogoSelector = false">取消</n-button>
|
|
||||||
<n-button
|
|
||||||
type="primary"
|
|
||||||
@click="confirmSelection"
|
|
||||||
:disabled="!selectedFileId"
|
|
||||||
>
|
|
||||||
确认选择
|
|
||||||
</n-button>
|
|
||||||
</n-space>
|
|
||||||
</template>
|
|
||||||
</n-modal>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
@@ -285,6 +241,9 @@ definePageMeta({
|
|||||||
|
|
||||||
import { useImageUrl } from '~/composables/useImageUrl'
|
import { useImageUrl } from '~/composables/useImageUrl'
|
||||||
import { useConfigChangeDetection } from '~/composables/useConfigChangeDetection'
|
import { useConfigChangeDetection } from '~/composables/useConfigChangeDetection'
|
||||||
|
import AnnouncementConfig from '~/components/Admin/AnnouncementConfig.vue'
|
||||||
|
import FloatButtonsConfig from '~/components/Admin/FloatButtonsConfig.vue'
|
||||||
|
import ImageSelectorModal from '~/components/Admin/ImageSelectorModal.vue'
|
||||||
|
|
||||||
const notification = useNotification()
|
const notification = useNotification()
|
||||||
const { getImageUrl } = useImageUrl()
|
const { getImageUrl } = useImageUrl()
|
||||||
@@ -294,18 +253,16 @@ const activeTab = ref('basic')
|
|||||||
|
|
||||||
// Logo选择器相关数据
|
// Logo选择器相关数据
|
||||||
const showLogoSelector = ref(false)
|
const showLogoSelector = ref(false)
|
||||||
const loading = ref(false)
|
|
||||||
const fileList = ref<any[]>([])
|
|
||||||
const selectedFileId = ref<number | null>(null)
|
|
||||||
const searchKeyword = ref('')
|
|
||||||
|
|
||||||
// 分页
|
// 微信和Telegram选择器相关数据
|
||||||
const pagination = ref({
|
const showWechatSelector = ref(false)
|
||||||
page: 1,
|
const showTelegramSelector = ref(false)
|
||||||
pageSize: 20,
|
|
||||||
total: 0,
|
// 公告类型接口
|
||||||
pageSizes: [10, 20, 50, 100]
|
interface Announcement {
|
||||||
})
|
content: string
|
||||||
|
enabled: boolean
|
||||||
|
}
|
||||||
|
|
||||||
// 配置表单数据类型
|
// 配置表单数据类型
|
||||||
interface SiteConfigForm {
|
interface SiteConfigForm {
|
||||||
@@ -317,10 +274,39 @@ interface SiteConfigForm {
|
|||||||
maintenance_mode: boolean
|
maintenance_mode: boolean
|
||||||
enable_register: boolean
|
enable_register: boolean
|
||||||
forbidden_words: string
|
forbidden_words: string
|
||||||
enable_sitemap: boolean
|
enable_announcements: boolean
|
||||||
sitemap_update_frequency: string
|
announcements: Announcement[]
|
||||||
|
enable_float_buttons: boolean
|
||||||
|
wechat_search_image: string
|
||||||
|
telegram_qr_image: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 公告配置子组件数据
|
||||||
|
const announcementConfig = computed({
|
||||||
|
get: () => ({
|
||||||
|
enable_announcements: configForm.value.enable_announcements,
|
||||||
|
announcements: configForm.value.announcements
|
||||||
|
}),
|
||||||
|
set: (value: any) => {
|
||||||
|
configForm.value.enable_announcements = value.enable_announcements
|
||||||
|
configForm.value.announcements = value.announcements
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 浮动按钮配置子组件数据
|
||||||
|
const floatButtonsConfig = computed({
|
||||||
|
get: () => ({
|
||||||
|
enable_float_buttons: configForm.value.enable_float_buttons,
|
||||||
|
wechat_search_image: configForm.value.wechat_search_image,
|
||||||
|
telegram_qr_image: configForm.value.telegram_qr_image
|
||||||
|
}),
|
||||||
|
set: (value: any) => {
|
||||||
|
configForm.value.enable_float_buttons = value.enable_float_buttons
|
||||||
|
configForm.value.wechat_search_image = value.wechat_search_image
|
||||||
|
configForm.value.telegram_qr_image = value.telegram_qr_image
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// 使用配置改动检测
|
// 使用配置改动检测
|
||||||
const {
|
const {
|
||||||
setOriginalConfig,
|
setOriginalConfig,
|
||||||
@@ -342,11 +328,16 @@ const {
|
|||||||
maintenance_mode: 'maintenance_mode',
|
maintenance_mode: 'maintenance_mode',
|
||||||
enable_register: 'enable_register',
|
enable_register: 'enable_register',
|
||||||
forbidden_words: 'forbidden_words',
|
forbidden_words: 'forbidden_words',
|
||||||
enable_sitemap: 'enable_sitemap',
|
enable_announcements: 'enable_announcements',
|
||||||
sitemap_update_frequency: 'sitemap_update_frequency'
|
announcements: 'announcements',
|
||||||
|
enable_float_buttons: 'enable_float_buttons',
|
||||||
|
wechat_search_image: 'wechat_search_image',
|
||||||
|
telegram_qr_image: 'telegram_qr_image'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 公告类型选项(如果需要的话可以保留,但根据反馈暂时移除)
|
||||||
|
|
||||||
// 配置表单数据
|
// 配置表单数据
|
||||||
const configForm = ref<SiteConfigForm>({
|
const configForm = ref<SiteConfigForm>({
|
||||||
site_title: '',
|
site_title: '',
|
||||||
@@ -357,8 +348,11 @@ const configForm = ref<SiteConfigForm>({
|
|||||||
maintenance_mode: false,
|
maintenance_mode: false,
|
||||||
enable_register: false,
|
enable_register: false,
|
||||||
forbidden_words: '',
|
forbidden_words: '',
|
||||||
enable_sitemap: false,
|
enable_announcements: false,
|
||||||
sitemap_update_frequency: 'daily'
|
announcements: [],
|
||||||
|
enable_float_buttons: false,
|
||||||
|
wechat_search_image: '',
|
||||||
|
telegram_qr_image: ''
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@@ -394,10 +388,13 @@ const fetchConfig = async () => {
|
|||||||
maintenance_mode: response.maintenance_mode || false,
|
maintenance_mode: response.maintenance_mode || false,
|
||||||
enable_register: response.enable_register || false,
|
enable_register: response.enable_register || false,
|
||||||
forbidden_words: response.forbidden_words || '',
|
forbidden_words: response.forbidden_words || '',
|
||||||
enable_sitemap: response.enable_sitemap || false,
|
enable_announcements: response.enable_announcements || false,
|
||||||
sitemap_update_frequency: response.sitemap_update_frequency || 'daily'
|
announcements: response.announcements ? JSON.parse(response.announcements) : [],
|
||||||
|
enable_float_buttons: response.enable_float_buttons || false,
|
||||||
|
wechat_search_image: response.wechat_search_image || '',
|
||||||
|
telegram_qr_image: response.telegram_qr_image || ''
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置表单数据和原始数据
|
// 设置表单数据和原始数据
|
||||||
configForm.value = { ...configData }
|
configForm.value = { ...configData }
|
||||||
setOriginalConfig(configData)
|
setOriginalConfig(configData)
|
||||||
@@ -421,18 +418,7 @@ const saveConfig = async () => {
|
|||||||
saving.value = true
|
saving.value = true
|
||||||
|
|
||||||
// 更新当前配置数据
|
// 更新当前配置数据
|
||||||
updateCurrentConfig({
|
updateCurrentConfig(configForm.value)
|
||||||
site_title: configForm.value.site_title,
|
|
||||||
site_description: configForm.value.site_description,
|
|
||||||
keywords: configForm.value.keywords,
|
|
||||||
copyright: configForm.value.copyright,
|
|
||||||
site_logo: configForm.value.site_logo,
|
|
||||||
maintenance_mode: configForm.value.maintenance_mode,
|
|
||||||
enable_register: configForm.value.enable_register,
|
|
||||||
forbidden_words: configForm.value.forbidden_words,
|
|
||||||
enable_sitemap: configForm.value.enable_sitemap,
|
|
||||||
sitemap_update_frequency: configForm.value.sitemap_update_frequency
|
|
||||||
})
|
|
||||||
|
|
||||||
const { useSystemConfigApi } = await import('~/composables/useApi')
|
const { useSystemConfigApi } = await import('~/composables/useApi')
|
||||||
const systemConfigApi = useSystemConfigApi()
|
const systemConfigApi = useSystemConfigApi()
|
||||||
@@ -481,99 +467,40 @@ const saveConfig = async () => {
|
|||||||
// Logo选择器方法
|
// Logo选择器方法
|
||||||
const openLogoSelector = () => {
|
const openLogoSelector = () => {
|
||||||
showLogoSelector.value = true
|
showLogoSelector.value = true
|
||||||
loadFileList()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const clearLogo = () => {
|
const clearLogo = () => {
|
||||||
configForm.value.site_logo = ''
|
configForm.value.site_logo = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadFileList = async () => {
|
// 子组件更新处理方法
|
||||||
try {
|
const handleAnnouncementUpdate = (newValue: any) => {
|
||||||
loading.value = true
|
configForm.value.enable_announcements = newValue.enable_announcements
|
||||||
const { useFileApi } = await import('~/composables/useFileApi')
|
configForm.value.announcements = newValue.announcements
|
||||||
const fileApi = useFileApi()
|
|
||||||
|
|
||||||
const response = await fileApi.getFileList({
|
|
||||||
page: pagination.value.page,
|
|
||||||
pageSize: pagination.value.pageSize,
|
|
||||||
search: searchKeyword.value,
|
|
||||||
fileType: 'image', // 只获取图片文件
|
|
||||||
status: 'active' // 只获取正常状态的文件
|
|
||||||
}) as any
|
|
||||||
|
|
||||||
if (response && response.data) {
|
|
||||||
fileList.value = response.data.files || []
|
|
||||||
pagination.value.total = response.data.total || 0
|
|
||||||
console.log('获取到的图片文件:', fileList.value) // 调试信息
|
|
||||||
|
|
||||||
// 添加图片URL处理调试
|
|
||||||
fileList.value.forEach(file => {
|
|
||||||
console.log('图片文件详情:', {
|
|
||||||
id: file.id,
|
|
||||||
name: file.original_name,
|
|
||||||
accessUrl: file.access_url,
|
|
||||||
processedUrl: getImageUrl(file.access_url),
|
|
||||||
fileType: file.file_type,
|
|
||||||
mimeType: file.mime_type
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('获取文件列表失败:', error)
|
|
||||||
notification.error({
|
|
||||||
content: '获取文件列表失败',
|
|
||||||
duration: 3000
|
|
||||||
})
|
|
||||||
} finally {
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSearch = () => {
|
const handleFloatButtonsUpdate = (newValue: any) => {
|
||||||
pagination.value.page = 1
|
configForm.value.enable_float_buttons = newValue.enable_float_buttons
|
||||||
loadFileList()
|
configForm.value.wechat_search_image = newValue.wechat_search_image
|
||||||
|
configForm.value.telegram_qr_image = newValue.telegram_qr_image
|
||||||
}
|
}
|
||||||
|
|
||||||
const handlePageChange = (page: number) => {
|
// Logo选择处理
|
||||||
pagination.value.page = page
|
const handleLogoSelect = (file: any) => {
|
||||||
loadFileList()
|
configForm.value.site_logo = file.access_url
|
||||||
|
showLogoSelector.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
const handlePageSizeChange = (pageSize: number) => {
|
// 微信图片选择处理
|
||||||
pagination.value.pageSize = pageSize
|
const handleWechatImageSelect = (file: any) => {
|
||||||
pagination.value.page = 1
|
configForm.value.wechat_search_image = file.access_url
|
||||||
loadFileList()
|
showWechatSelector.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectFile = (file: any) => {
|
// Telegram图片选择处理
|
||||||
selectedFileId.value = file.id
|
const handleTelegramImageSelect = (file: any) => {
|
||||||
}
|
configForm.value.telegram_qr_image = file.access_url
|
||||||
|
showTelegramSelector.value = false
|
||||||
const confirmSelection = () => {
|
|
||||||
if (selectedFileId.value) {
|
|
||||||
const file = fileList.value.find(f => f.id === selectedFileId.value)
|
|
||||||
if (file) {
|
|
||||||
configForm.value.site_logo = file.access_url
|
|
||||||
showLogoSelector.value = false
|
|
||||||
selectedFileId.value = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const formatFileSize = (size: number) => {
|
|
||||||
if (size < 1024) return size + ' B'
|
|
||||||
if (size < 1024 * 1024) return (size / 1024).toFixed(1) + ' KB'
|
|
||||||
if (size < 1024 * 1024 * 1024) return (size / (1024 * 1024)).toFixed(1) + ' MB'
|
|
||||||
return (size / (1024 * 1024 * 1024)).toFixed(1) + ' GB'
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleImageError = (event: any) => {
|
|
||||||
console.error('图片加载失败:', event)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleImageLoad = (event: any) => {
|
|
||||||
console.log('图片加载成功:', event)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 页面加载时获取配置
|
// 页面加载时获取配置
|
||||||
@@ -610,35 +537,4 @@ onMounted(() => {
|
|||||||
padding-bottom: 1rem;
|
padding-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
|
||||||
gap: 1rem;
|
|
||||||
max-height: 400px;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-item {
|
|
||||||
border: 1px solid #e5e7eb;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-item:hover {
|
|
||||||
transform: translateY(-2px);
|
|
||||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview-image {
|
|
||||||
width: 100%;
|
|
||||||
height: 120px;
|
|
||||||
object-fit: cover;
|
|
||||||
border: 1px solid #e5e7eb;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pagination-wrapper {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
margin-top: 1rem;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
@@ -73,6 +73,11 @@
|
|||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 公告信息 -->
|
||||||
|
<div class="w-full max-w-3xl mx-auto mb-2 px-2 sm:px-0">
|
||||||
|
<Announcement />
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 搜索区域 -->
|
<!-- 搜索区域 -->
|
||||||
<div class="w-full max-w-3xl mx-auto mb-4 sm:mb-8 px-2 sm:px-0">
|
<div class="w-full max-w-3xl mx-auto mb-4 sm:mb-8 px-2 sm:px-0">
|
||||||
<ClientOnly>
|
<ClientOnly>
|
||||||
@@ -84,7 +89,7 @@
|
|||||||
</n-input>
|
</n-input>
|
||||||
</div>
|
</div>
|
||||||
</ClientOnly>
|
</ClientOnly>
|
||||||
|
|
||||||
<!-- 平台类型筛选 -->
|
<!-- 平台类型筛选 -->
|
||||||
<div class="mt-3 flex flex-wrap gap-2" id="platformFilters">
|
<div class="mt-3 flex flex-wrap gap-2" id="platformFilters">
|
||||||
<a
|
<a
|
||||||
|
|||||||
Reference in New Issue
Block a user