Skip to content

Commit 66d3f52

Browse files
authored
增加弹窗功能 (#492)
* feat: add Popup feature * fix: undefined error
1 parent a9bfc16 commit 66d3f52

File tree

12 files changed

+234
-6
lines changed

12 files changed

+234
-6
lines changed

service/src/index.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import type { ChatMessage } from './chatgpt'
1111
import { abortChatProcess, chatConfig, chatReplyProcess, containsSensitiveWords, initAuditService } from './chatgpt'
1212
import { auth, getUserId } from './middleware/auth'
1313
import { clearApiKeyCache, clearConfigCache, getApiKeys, getCacheApiKeys, getCacheConfig, getOriginConfig } from './storage/config'
14-
import type { AuditConfig, ChatInfo, ChatOptions, Config, KeyConfig, MailConfig, SiteConfig, UserConfig, UserInfo } from './storage/model'
14+
import type { AnnounceConfig, AuditConfig, ChatInfo, ChatOptions, Config, KeyConfig, MailConfig, SiteConfig, UserConfig, UserInfo } from './storage/model'
1515
import { AdvancedConfig, Status, UsageResponse, UserRole } from './storage/model'
1616
import {
1717
clearChat,
@@ -1175,6 +1175,30 @@ router.post('/mail-test', rootAuth, async (req, res) => {
11751175
}
11761176
})
11771177

1178+
router.post('/setting-announce', rootAuth, async (req, res) => {
1179+
try {
1180+
const config = req.body as AnnounceConfig
1181+
const thisConfig = await getOriginConfig()
1182+
thisConfig.announceConfig = config
1183+
const result = await updateConfig(thisConfig)
1184+
clearConfigCache()
1185+
res.send({ status: 'Success', message: '操作成功 | Successfully', data: result.announceConfig })
1186+
}
1187+
catch (error) {
1188+
res.send({ status: 'Fail', message: error.message, data: null })
1189+
}
1190+
})
1191+
1192+
router.post('/announcement', async (req, res) => {
1193+
try {
1194+
const result = await getCacheConfig()
1195+
res.send({ status: 'Success', message: '操作成功 | Successfully', data: result.announceConfig })
1196+
}
1197+
catch (error) {
1198+
res.send({ status: 'Fail', message: error.message, data: null })
1199+
}
1200+
})
1201+
11781202
router.post('/setting-audit', rootAuth, async (req, res) => {
11791203
try {
11801204
const config = req.body as AuditConfig

service/src/storage/config.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { ObjectId } from 'mongodb'
22
import * as dotenv from 'dotenv'
33
import type { TextAuditServiceProvider } from 'src/utils/textAudit'
44
import { isNotEmptyString, isTextAuditServiceProvider } from '../utils/is'
5-
import { AdvancedConfig, AuditConfig, Config, KeyConfig, MailConfig, SiteConfig, TextAudioType, UserRole } from './model'
5+
import { AdvancedConfig, AnnounceConfig, AuditConfig, Config, KeyConfig, MailConfig, SiteConfig, TextAudioType, UserRole } from './model'
66
import { getConfig, getKeys, upsertKey } from './mongo'
77

88
dotenv.config()
@@ -106,6 +106,13 @@ export async function getOriginConfig() {
106106
)
107107
}
108108

109+
if (!config.announceConfig) {
110+
config.announceConfig = new AnnounceConfig(
111+
false,
112+
'',
113+
)
114+
}
115+
109116
if (!isNotEmptyString(config.siteConfig.chatModels))
110117
config.siteConfig.chatModels = 'gpt-3.5-turbo,gpt-4-turbo-preview,gpt-4-vision-preview'
111118

service/src/storage/model.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,7 @@ export class Config {
188188
public mailConfig?: MailConfig,
189189
public auditConfig?: AuditConfig,
190190
public advancedConfig?: AdvancedConfig,
191+
public announceConfig?: AnnounceConfig,
191192
) { }
192193
}
193194

@@ -207,6 +208,13 @@ export class SiteConfig {
207208
) { }
208209
}
209210

211+
export class AnnounceConfig {
212+
constructor(
213+
public enabled: boolean,
214+
public announceWords: string,
215+
) { }
216+
}
217+
210218
export class MailConfig {
211219
constructor(
212220
public smtpHost: string,

src/api/index.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
11
import type { AxiosProgressEvent, GenericAbortSignal } from 'axios'
22
import { get, post } from '@/utils/request'
3-
import type { AuditConfig, ConfigState, KeyConfig, MailConfig, SiteConfig, Status, UserInfo, UserPassword } from '@/components/common/Setting/model'
3+
import type { AnnounceConfig, AuditConfig, ConfigState, KeyConfig, MailConfig, SiteConfig, Status, UserInfo, UserPassword } from '@/components/common/Setting/model'
44
import { useAuthStore, useUserStore } from '@/store'
55
import type { SettingsState } from '@/store/modules/user/helper'
66

7+
export function fetchAnnouncement<T = any>() {
8+
return post<T>({
9+
url: '/announcement',
10+
})
11+
}
12+
713
export function fetchChatConfig<T = any>() {
814
return post<T>({
915
url: '/config',
@@ -313,6 +319,13 @@ export function fetchTestAudit<T = any>(text: string, audit: AuditConfig) {
313319
})
314320
}
315321

322+
export function fetchUpdateAnnounce<T = any>(announce: AnnounceConfig) {
323+
return post<T>({
324+
url: '/setting-announce',
325+
data: announce,
326+
})
327+
}
328+
316329
export function fetchUpdateAdvanced<T = any>(sync: boolean, advanced: SettingsState) {
317330
const data = { sync, ...advanced }
318331
return post<T>({
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
<script setup lang='ts'>
2+
import { onMounted, ref } from 'vue'
3+
import { NButton, NInput, NSpin, NSwitch, useMessage } from 'naive-ui'
4+
import { AnnounceConfig, ConfigState } from './model'
5+
import { fetchChatConfig, fetchUpdateAnnounce } from '@/api'
6+
import { t } from '@/locales'
7+
8+
const ms = useMessage()
9+
10+
const loading = ref(false)
11+
const saving = ref(false)
12+
13+
const config = ref<AnnounceConfig>()
14+
15+
async function fetchConfig() {
16+
try {
17+
loading.value = true
18+
const { data } = await fetchChatConfig<ConfigState>()
19+
config.value = data.announceConfig
20+
}
21+
finally {
22+
loading.value = false
23+
}
24+
}
25+
26+
async function updateAnnouncement() {
27+
saving.value = true
28+
try {
29+
const { data } = await fetchUpdateAnnounce(config.value as AnnounceConfig)
30+
config.value = data
31+
ms.success(t('common.success'))
32+
}
33+
catch (error: any) {
34+
ms.error(error.message)
35+
}
36+
saving.value = false
37+
}
38+
39+
onMounted(() => {
40+
fetchConfig()
41+
})
42+
</script>
43+
44+
<template>
45+
<NSpin :show="loading">
46+
<div class="space-y-6">
47+
<div class="flex items-center space-x-4">
48+
<span class="flex-shrink-0 w-[100px]">{{ $t('setting.announceEnabled') }}</span>
49+
<div class="flex-1">
50+
<NSwitch
51+
:round="false" :value="config && config.enabled"
52+
@update:value="(val) => { if (config) config.enabled = val }"
53+
/>
54+
</div>
55+
</div>
56+
<div class="flex items-center space-x-4">
57+
<span class="flex-shrink-0 w-[100px]">{{ $t('setting.announceWords') }}</span>
58+
<div class="flex-1">
59+
<NInput
60+
:value="config && config.announceWords"
61+
placeholder="输入公告内容 | Set AnnouncementWords"
62+
type="textarea"
63+
:autosize="{ minRows: 1, maxRows: 10 }"
64+
@input="(val) => { if (config) config.announceWords = val }"
65+
/>
66+
</div>
67+
</div>
68+
<span class="flex-shrink-0 w-[100px]" />
69+
<div class="flex flex-wrap items-center gap-4">
70+
<NButton :loading="saving" type="primary" @click="updateAnnouncement()">
71+
{{ $t('common.save') }}
72+
</NButton>
73+
</div>
74+
</div>
75+
</NSpin>
76+
</template>

src/components/common/Setting/index.vue

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import User from './User.vue'
1212
import Key from './Keys.vue'
1313
import Password from './Password.vue'
1414
import TwoFA from './TwoFA.vue'
15+
import Announcement from './Anonuncement.vue'
1516
import { SvgIcon } from '@/components/common'
1617
import { useAuthStore, useUserStore } from '@/store'
1718
import { useBasicLayout } from '@/hooks/useBasicLayout'
@@ -105,6 +106,13 @@ const show = computed({
105106
</template>
106107
<Site />
107108
</NTabPane>
109+
<NTabPane v-if="userStore.userInfo.root" name="AnnounceConfig" tab="AnnounceConfig">
110+
<template #tab>
111+
<SvgIcon class="text-lg" icon="ri:settings-line" />
112+
<span class="ml-2">{{ $t('setting.announceConfig') }}</span>
113+
</template>
114+
<Announcement />
115+
</NTabPane>
108116
<NTabPane v-if="userStore.userInfo.root" name="MailConfig" tab="MailConfig">
109117
<template #tab>
110118
<SvgIcon class="text-lg" icon="ri:mail-line" />

src/components/common/Setting/model.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export class ConfigState {
1313
siteConfig?: SiteConfig
1414
mailConfig?: MailConfig
1515
auditConfig?: AuditConfig
16+
announceConfig?: AnnounceConfig
1617
}
1718

1819
export class UserConfig {
@@ -63,6 +64,11 @@ export class AuditConfig {
6364
sensitiveWords?: string
6465
}
6566

67+
export class AnnounceConfig {
68+
enabled?: boolean
69+
announceWords?: string
70+
}
71+
6672
export enum Status {
6773
Normal = 0,
6874
Deleted = 1,

src/locales/en-US.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,9 @@ export default {
8080
disable2FAConfirm: 'Are you sure to disable 2FA for this user?',
8181
},
8282
setting: {
83+
announceConfig: 'Announcement',
84+
announceEnabled: 'Open Announcement',
85+
announceWords: 'Announcement Content',
8386
globalAmount: 'Global Usage Amount for New User',
8487
limit_switch: 'Open Usage Limitation',
8588
usageCountLimit: 'Enable Usage Count Limit',

src/locales/ko-KR.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,9 @@ export default {
8080
disable2FAConfirm: 'Are you sure to disable 2FA for this user?',
8181
},
8282
setting: {
83+
announceConfig: '网站公告',
84+
announceEnabled: 'Open Announcement',
85+
announceWords: 'Announcement Content',
8386
globalAmount: 'Global Usage Amount for New User',
8487
limit_switch: '오픈 횟수 제한',
8588
redeemCardNo: '질문 허용 횟수',

src/locales/zh-CN.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,9 @@ export default {
8080
disable2FAConfirm: '您确定要为此用户禁用两步验证吗??',
8181
},
8282
setting: {
83+
announceConfig: '网站公告',
84+
announceEnabled: '公告开关',
85+
announceWords: '公告内容',
8386
globalAmount: '新用户全局次数设置',
8487
limit_switch: '打开次数限制',
8588
usageCountLimit: '使用次数限制',

src/locales/zh-TW.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,9 @@ export default {
8080
disable2FAConfirm: '您确定要为此用户禁用两步验证吗??',
8181
},
8282
setting: {
83+
announceConfig: '网站公告',
84+
announceEnabled: '打开公告',
85+
announceWords: '公告内容',
8386
globalAmount: '新用户全局次数设置',
8487
limit_switch: '開啟次數限制',
8588
redeemCardNo: '兌換碼卡號',

src/views/chat/layout/sider/index.vue

Lines changed: 77 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
<script setup lang='ts'>
2-
import type { CSSProperties } from 'vue'
3-
import { computed, ref, watch } from 'vue'
4-
import { NButton, NLayoutSider } from 'naive-ui'
2+
import { CSSProperties, computed, onMounted, ref, watch } from 'vue'
3+
import { NButton, NLayoutSider, NModal } from 'naive-ui'
54
import List from './List.vue'
65
import Footer from './Footer.vue'
76
import { useAppStore, useAuthStore, useChatStore } from '@/store'
87
import { useBasicLayout } from '@/hooks/useBasicLayout'
98
import { GithubSite, PromptStore } from '@/components/common'
9+
import { fetchAnnouncement } from '@/api'
10+
import { AnnounceConfig } from '@/components/common/Setting/model'
11+
12+
const config = ref<AnnounceConfig>()
1013
1114
const appStore = useAppStore()
1215
const authStore = useAuthStore()
@@ -56,6 +59,48 @@ watch(
5659
flush: 'post',
5760
},
5861
)
62+
63+
const notice_text = ref('')
64+
const showNotice = ref(false)
65+
66+
const closeModal = () => {
67+
showNotice.value = false
68+
}
69+
70+
const doNotShowToday = () => {
71+
const today = new Date().toDateString()
72+
localStorage.setItem('announcementLastClosed', today)
73+
closeModal()
74+
}
75+
76+
const checkDoNotShowToday = () => {
77+
const today = new Date().toDateString()
78+
const lastClosed = localStorage.getItem('announcementLastClosed')
79+
if (lastClosed === today)
80+
showNotice.value = false
81+
}
82+
83+
async function fetchAnnounce() {
84+
try {
85+
// 从数据库获取公告配置
86+
const { data } = await fetchAnnouncement()
87+
config.value = data
88+
if (config.value) {
89+
if (config.value.enabled)
90+
showNotice.value = true
91+
checkDoNotShowToday()
92+
return config.value
93+
}
94+
}
95+
catch (error) {
96+
console.error('Error fetching the announcement:', error)
97+
}
98+
}
99+
100+
onMounted(async () => {
101+
const data = await fetchAnnounce()
102+
notice_text.value = `${data?.announceWords}`
103+
})
59104
</script>
60105

61106
<template>
@@ -94,4 +139,33 @@ watch(
94139
<div v-show="!collapsed" class="fixed inset-0 z-40 bg-black/40" @click="handleUpdateCollapsed" />
95140
</template>
96141
<PromptStore v-model:visible="show" />
142+
<NModal v-model:show="showNotice" :auto-focus="false" preset="card" :style="{ width: !isMobile ? '33%' : '90%' }">
143+
<div class="p-4 space-y-5 min-h-[200px]">
144+
<div class="w-full markdown-body" v-html="notice_text" />
145+
</div>
146+
<div class="buttons-container">
147+
<div class="button-wrapper">
148+
<NButton type="primary" @click="closeModal">
149+
关闭公告
150+
</NButton>
151+
</div>
152+
<div class="button-wrapper">
153+
<NButton type="default" @click="doNotShowToday">
154+
今日不再提示
155+
</NButton>
156+
</div>
157+
</div>
158+
</NModal>
97159
</template>
160+
161+
<style scoped>
162+
.buttons-container {
163+
display: flex;
164+
justify-content: flex-end;
165+
align-items: flex-end;
166+
}
167+
168+
.button-wrapper {
169+
margin-left: 10px;
170+
}
171+
</style>

0 commit comments

Comments
 (0)