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
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
@@ -90,6 +91,25 @@ func SystemConfigToResponse(configs []entity.SystemConfig) *dto.SystemConfigResp
|
||||
response.MeilisearchMasterKey = config.Value
|
||||
case entity.ConfigKeyMeilisearchIndexName:
|
||||
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)
|
||||
}
|
||||
|
||||
// 界面配置处理
|
||||
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 {
|
||||
utils.Info("配置更新 - 被修改的配置项: %v", updatedKeys)
|
||||
@@ -332,6 +377,24 @@ func SystemConfigToPublicResponse(configs []entity.SystemConfig) map[string]inte
|
||||
response[entity.ConfigResponseFieldMeilisearchMasterKey] = config.Value
|
||||
case entity.ConfigKeyMeilisearchIndexName:
|
||||
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,
|
||||
MeilisearchMasterKey: entity.ConfigDefaultMeilisearchMasterKey,
|
||||
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"`
|
||||
MeilisearchMasterKey *string `json:"meilisearch_master_key,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 系统配置响应
|
||||
@@ -90,6 +97,13 @@ type SystemConfigResponse struct {
|
||||
MeilisearchPort string `json:"meilisearch_port"`
|
||||
MeilisearchMasterKey string `json:"meilisearch_master_key"`
|
||||
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 单个配置项
|
||||
|
||||
@@ -56,6 +56,13 @@ const (
|
||||
ConfigKeyTelegramProxyPort = "telegram_proxy_port"
|
||||
ConfigKeyTelegramProxyUsername = "telegram_proxy_username"
|
||||
ConfigKeyTelegramProxyPassword = "telegram_proxy_password"
|
||||
|
||||
// 界面配置
|
||||
ConfigKeyEnableAnnouncements = "enable_announcements"
|
||||
ConfigKeyAnnouncements = "announcements"
|
||||
ConfigKeyEnableFloatButtons = "enable_float_buttons"
|
||||
ConfigKeyWechatSearchImage = "wechat_search_image"
|
||||
ConfigKeyTelegramQrImage = "telegram_qr_image"
|
||||
)
|
||||
|
||||
// ConfigType 配置类型常量
|
||||
@@ -183,4 +190,11 @@ const (
|
||||
ConfigDefaultTelegramProxyPort = "8080"
|
||||
ConfigDefaultTelegramProxyUsername = ""
|
||||
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.ConfigKeyMeilisearchMasterKey, Value: entity.ConfigDefaultMeilisearchMasterKey, 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)
|
||||
@@ -169,6 +174,11 @@ func (r *SystemConfigRepositoryImpl) GetOrCreateDefault() ([]entity.SystemConfig
|
||||
entity.ConfigKeyMeilisearchPort: {Key: entity.ConfigKeyMeilisearchPort, Value: entity.ConfigDefaultMeilisearchPort, 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.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) {
|
||||
var req dto.SystemConfigRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
utils.Error("JSON绑定失败: %v", err)
|
||||
ErrorResponse(c, "请求参数错误", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
@@ -141,31 +142,53 @@ func UpdateSystemConfig(c *gin.Context) {
|
||||
}
|
||||
|
||||
// 验证参数 - 只验证提交的字段
|
||||
if req.SiteTitle != nil && (len(*req.SiteTitle) < 1 || len(*req.SiteTitle) > 100) {
|
||||
utils.Info("开始验证参数")
|
||||
if req.SiteTitle != nil {
|
||||
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 {
|
||||
utils.Info("验证AutoProcessInterval: %d", *req.AutoProcessInterval)
|
||||
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 {
|
||||
utils.Info("验证PageSize: %d", *req.PageSize)
|
||||
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 {
|
||||
utils.Info("验证AutoTransferLimitDays: %d", *req.AutoTransferLimitDays)
|
||||
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 {
|
||||
utils.Info("验证AutoTransferMinSpace: %d", *req.AutoTransferMinSpace)
|
||||
if *req.AutoTransferMinSpace < 100 || *req.AutoTransferMinSpace > 1024 {
|
||||
ErrorResponse(c, "最小存储空间必须在100-1024GB之间", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 验证公告相关字段
|
||||
if req.Announcements != nil {
|
||||
utils.Info("验证Announcements: '%s'", *req.Announcements)
|
||||
// 可以在这里添加更详细的验证逻辑
|
||||
}
|
||||
|
||||
// 转换为实体
|
||||
configs := converter.RequestToSystemConfig(&req)
|
||||
|
||||
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']
|
||||
NList: typeof import('naive-ui')['NList']
|
||||
NListItem: typeof import('naive-ui')['NListItem']
|
||||
NMarquee: typeof import('naive-ui')['NMarquee']
|
||||
NMessageProvider: typeof import('naive-ui')['NMessageProvider']
|
||||
NModal: typeof import('naive-ui')['NModal']
|
||||
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",
|
||||
"@vicons/ionicons5": "^0.12.0",
|
||||
"chart.js": "^4.5.0",
|
||||
"naive-ui": "^2.37.0",
|
||||
"naive-ui": "^2.42.0",
|
||||
"pinia": "^2.1.0",
|
||||
"vfonts": "^0.0.3",
|
||||
"vue": "^3.3.0",
|
||||
|
||||
@@ -176,103 +176,59 @@
|
||||
</n-form>
|
||||
</div>
|
||||
</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>
|
||||
</div>
|
||||
</template>
|
||||
</AdminPageLayout>
|
||||
<!-- Logo选择模态框 -->
|
||||
<n-modal v-model:show="showLogoSelector" preset="card" title="选择Logo图片" 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"
|
||||
<!-- ImageSelectorModal 组件 -->
|
||||
<ImageSelectorModal
|
||||
v-model:show="showLogoSelector"
|
||||
title="选择Logo图片"
|
||||
@select="handleLogoSelect"
|
||||
/>
|
||||
|
||||
<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>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -285,6 +241,9 @@ definePageMeta({
|
||||
|
||||
import { useImageUrl } from '~/composables/useImageUrl'
|
||||
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 { getImageUrl } = useImageUrl()
|
||||
@@ -294,18 +253,16 @@ const activeTab = ref('basic')
|
||||
|
||||
// Logo选择器相关数据
|
||||
const showLogoSelector = ref(false)
|
||||
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]
|
||||
})
|
||||
// 微信和Telegram选择器相关数据
|
||||
const showWechatSelector = ref(false)
|
||||
const showTelegramSelector = ref(false)
|
||||
|
||||
// 公告类型接口
|
||||
interface Announcement {
|
||||
content: string
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
// 配置表单数据类型
|
||||
interface SiteConfigForm {
|
||||
@@ -317,10 +274,39 @@ interface SiteConfigForm {
|
||||
maintenance_mode: boolean
|
||||
enable_register: boolean
|
||||
forbidden_words: string
|
||||
enable_sitemap: boolean
|
||||
sitemap_update_frequency: string
|
||||
enable_announcements: boolean
|
||||
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 {
|
||||
setOriginalConfig,
|
||||
@@ -342,11 +328,16 @@ const {
|
||||
maintenance_mode: 'maintenance_mode',
|
||||
enable_register: 'enable_register',
|
||||
forbidden_words: 'forbidden_words',
|
||||
enable_sitemap: 'enable_sitemap',
|
||||
sitemap_update_frequency: 'sitemap_update_frequency'
|
||||
enable_announcements: 'enable_announcements',
|
||||
announcements: 'announcements',
|
||||
enable_float_buttons: 'enable_float_buttons',
|
||||
wechat_search_image: 'wechat_search_image',
|
||||
telegram_qr_image: 'telegram_qr_image'
|
||||
}
|
||||
})
|
||||
|
||||
// 公告类型选项(如果需要的话可以保留,但根据反馈暂时移除)
|
||||
|
||||
// 配置表单数据
|
||||
const configForm = ref<SiteConfigForm>({
|
||||
site_title: '',
|
||||
@@ -357,8 +348,11 @@ const configForm = ref<SiteConfigForm>({
|
||||
maintenance_mode: false,
|
||||
enable_register: false,
|
||||
forbidden_words: '',
|
||||
enable_sitemap: false,
|
||||
sitemap_update_frequency: 'daily'
|
||||
enable_announcements: false,
|
||||
announcements: [],
|
||||
enable_float_buttons: false,
|
||||
wechat_search_image: '',
|
||||
telegram_qr_image: ''
|
||||
})
|
||||
|
||||
|
||||
@@ -394,8 +388,11 @@ const fetchConfig = async () => {
|
||||
maintenance_mode: response.maintenance_mode || false,
|
||||
enable_register: response.enable_register || false,
|
||||
forbidden_words: response.forbidden_words || '',
|
||||
enable_sitemap: response.enable_sitemap || false,
|
||||
sitemap_update_frequency: response.sitemap_update_frequency || 'daily'
|
||||
enable_announcements: response.enable_announcements || false,
|
||||
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 || ''
|
||||
}
|
||||
|
||||
// 设置表单数据和原始数据
|
||||
@@ -421,18 +418,7 @@ const saveConfig = async () => {
|
||||
saving.value = true
|
||||
|
||||
// 更新当前配置数据
|
||||
updateCurrentConfig({
|
||||
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
|
||||
})
|
||||
updateCurrentConfig(configForm.value)
|
||||
|
||||
const { useSystemConfigApi } = await import('~/composables/useApi')
|
||||
const systemConfigApi = useSystemConfigApi()
|
||||
@@ -481,99 +467,40 @@ const saveConfig = async () => {
|
||||
// Logo选择器方法
|
||||
const openLogoSelector = () => {
|
||||
showLogoSelector.value = true
|
||||
loadFileList()
|
||||
}
|
||||
|
||||
const clearLogo = () => {
|
||||
configForm.value.site_logo = ''
|
||||
}
|
||||
|
||||
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
|
||||
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 handleAnnouncementUpdate = (newValue: any) => {
|
||||
configForm.value.enable_announcements = newValue.enable_announcements
|
||||
configForm.value.announcements = newValue.announcements
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
pagination.value.page = 1
|
||||
loadFileList()
|
||||
const handleFloatButtonsUpdate = (newValue: any) => {
|
||||
configForm.value.enable_float_buttons = newValue.enable_float_buttons
|
||||
configForm.value.wechat_search_image = newValue.wechat_search_image
|
||||
configForm.value.telegram_qr_image = newValue.telegram_qr_image
|
||||
}
|
||||
|
||||
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) {
|
||||
// Logo选择处理
|
||||
const handleLogoSelect = (file: any) => {
|
||||
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 handleWechatImageSelect = (file: any) => {
|
||||
configForm.value.wechat_search_image = file.access_url
|
||||
showWechatSelector.value = false
|
||||
}
|
||||
|
||||
const handleImageError = (event: any) => {
|
||||
console.error('图片加载失败:', event)
|
||||
}
|
||||
|
||||
const handleImageLoad = (event: any) => {
|
||||
console.log('图片加载成功:', event)
|
||||
// Telegram图片选择处理
|
||||
const handleTelegramImageSelect = (file: any) => {
|
||||
configForm.value.telegram_qr_image = file.access_url
|
||||
showTelegramSelector.value = false
|
||||
}
|
||||
|
||||
// 页面加载时获取配置
|
||||
@@ -610,35 +537,4 @@ onMounted(() => {
|
||||
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>
|
||||
@@ -73,6 +73,11 @@
|
||||
</nav>
|
||||
</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">
|
||||
<ClientOnly>
|
||||
|
||||
Reference in New Issue
Block a user