1414 </a-space >
1515
1616 <a-divider />
17-
17+
1818 <div class =" item" >
1919 <div class =" label" >1.开启该设置时,会“隐藏”论坛左侧触发的设置按钮。</div >
2020 <a-switch v-model =" isShow" @change =" ShowSettingConfig" />
4545 </p >
4646 <a href =" javascript:void(0)" target =" _blank" @click =" gosidepanel" >点击显示侧边链接</a >
4747 </div >
48+
49+ <a-divider />
50+
51+ <div class =" site-access" >
52+ <div class =" site-access-title" >有权访问的网站</div >
53+ <p class =" site-access-desc" >
54+ 默认仅允许访问 linux.do 及其子域名。AI API、WebDAV 等外部服务,需要在这里单独授权对应站点。
55+ </p >
56+
57+ <div class =" site-access-group" >
58+ <div class =" group-title" >默认可访问网站</div >
59+ <div class =" site-list tags" >
60+ <span v-for =" site in defaultSites" :key =" site" class =" site-tag fixed" >{{ formatSiteLabel(site) }}</span >
61+ </div >
62+ </div >
63+
64+ <div class =" site-access-group" >
65+ <div class =" group-title" >已额外授权的网站</div >
66+ <div v-if =" optionalSites.length > 0" class =" site-list rows" >
67+ <div class =" site-row" v-for =" site in optionalSites" :key =" site" >
68+ <span class =" site-origin" >{{ formatSiteLabel(site) }}</span >
69+ <a-button size =" small" status =" danger" @click =" removeSiteAccess(site)" >移除</a-button >
70+ </div >
71+ </div >
72+ <div v-else class =" site-empty" >暂无额外授权站点</div >
73+ </div >
74+
75+ <div class =" site-access-form" >
76+ <input
77+ v-model.trim =" siteAccessInput"
78+ type =" text"
79+ placeholder =" https://api.example.com/v1/chat/completions"
80+ />
81+ <a-space >
82+ <a-button type =" primary" :loading =" siteAccessLoading" @click =" requestSiteAccess" >授权站点</a-button >
83+ <a-button :loading =" siteAccessRefreshing" @click =" loadAuthorizedSites" >刷新</a-button >
84+ </a-space >
85+ </div >
86+
87+ <div class =" site-access-tip" >
88+ 撤销站点权限后,依赖该站点的 AI 或 WebDAV 功能会立即失效,直到重新授权。
89+ </div >
90+ </div >
4891</template >
4992
5093<script >
5194import Question from " ./SVG/Question.vue" ;
95+ import { requestSitePermission } from " ../../utilities/sitePermissions.js" ;
5296
5397export default {
5498 components: {
@@ -61,9 +105,88 @@ export default {
61105 clickTarget: " sidepanel" ,
62106 activeKey: [" 1" ],
63107 CompatibilityReminder: false ,
108+ defaultSites: [" https://linux.do/*" , " https://*.linux.do/*" ],
109+ optionalSites: [],
110+ siteAccessInput: " " ,
111+ siteAccessLoading: false ,
112+ siteAccessRefreshing: false ,
64113 };
65114 },
66115 methods: {
116+ // 与 background 通信,读取或移除已授权站点
117+ sendRuntimeMessage (action , payload = {}) {
118+ const browserAPI = typeof browser !== " undefined" ? browser : chrome;
119+ return new Promise ((resolve , reject ) => {
120+ browserAPI .runtime .sendMessage ({ action, ... payload }, (response ) => {
121+ if (browserAPI .runtime .lastError ) {
122+ reject (new Error (browserAPI .runtime .lastError .message ));
123+ return ;
124+ }
125+ resolve (response);
126+ });
127+ });
128+ },
129+ formatSiteLabel (site ) {
130+ return site .replace (/ ^ https:\/\/ / , " " );
131+ },
132+ // 获取当前已授权的默认站点和额外站点
133+ async loadAuthorizedSites () {
134+ this .siteAccessRefreshing = true ;
135+ try {
136+ const response = await this .sendRuntimeMessage (" permissions_get_sites" );
137+ if (! response? .success ) {
138+ throw new Error (response? .error || " 获取已授权站点失败" );
139+ }
140+ this .defaultSites = response .defaultSites || this .defaultSites ;
141+ this .optionalSites = response .optionalSites || [];
142+ } catch (error) {
143+ this .$message .error (error .message || " 获取已授权站点失败" );
144+ } finally {
145+ this .siteAccessRefreshing = false ;
146+ }
147+ },
148+ // 必须在用户点击事件里直接申请权限,避免 Chrome MV3 丢失 user gesture
149+ async requestSiteAccess () {
150+ if (! this .siteAccessInput ) {
151+ this .$message .warning (" 请输入需要授权的站点 URL" );
152+ return ;
153+ }
154+
155+ this .siteAccessLoading = true ;
156+ try {
157+ const response = await requestSitePermission (this .siteAccessInput );
158+ if (! response? .success ) {
159+ throw new Error (response? .error || " 站点授权失败" );
160+ }
161+
162+ this .siteAccessInput = " " ;
163+ this .$message .success (response .origin ? ` 已授权 ${ response .origin } ` : " 已授权默认站点" );
164+ await this .loadAuthorizedSites ();
165+ } catch (error) {
166+ this .$message .error (error .message || " 站点授权失败" );
167+ } finally {
168+ this .siteAccessLoading = false ;
169+ }
170+ },
171+ // 移除某个额外授权站点
172+ async removeSiteAccess (site ) {
173+ if (! window .confirm (` 确定移除 ${ this .formatSiteLabel (site)} 的访问权限吗?` )) {
174+ return ;
175+ }
176+
177+ try {
178+ const response = await this .sendRuntimeMessage (" permissions_remove_site" , {
179+ origin: site,
180+ });
181+ if (! response? .success ) {
182+ throw new Error (response? .error || " 移除站点权限失败" );
183+ }
184+ this .$message .success (` 已移除 ${ response .origin } ` );
185+ await this .loadAuthorizedSites ();
186+ } catch (error) {
187+ this .$message .error (error .message || " 移除站点权限失败" );
188+ }
189+ },
67190 gosidepanel () {
68191 const browserAPI = typeof browser !== " undefined" ? browser : chrome;
69192 browserAPI .tabs .create ({
@@ -89,11 +212,9 @@ export default {
89212 url: browserAPI .runtime .getURL (" options.html" ),
90213 });
91214 },
92-
93215 goGithub () {
94216 window .open (" https://github.com/anghunk/linuxdo-scripts" , " _blank" );
95217 },
96-
97218 // 是否隐藏设置按钮
98219 ShowSettingConfig () {
99220 localStorage .setItem (" isShowSettingConfig" , this .isShow );
@@ -108,19 +229,16 @@ export default {
108229 this .$message .success (" 切换成功!" );
109230 });
110231 },
111-
112232 onClickTargetChange () {
113233 const browserAPI = typeof browser !== " undefined" ? browser : chrome;
114234 browserAPI .storage ? .local .set ({ clickOpenTarget: this .clickTarget }, () => {
115235 this .$message .success (" 已更新点击打开方式!" );
116236 });
117237 },
118-
119238 // 切换兼容性提示显示
120239 showCompatibilityReminder () {
121240 this .CompatibilityReminder = ! this .CompatibilityReminder ;
122241 },
123-
124242 // 将已读按钮放在最前方
125243 ReadonlyBefore () {
126244 try {
@@ -142,7 +260,7 @@ export default {
142260 }
143261 },
144262 },
145- created () {
263+ async created () {
146264 const isShowSettingConfig = localStorage .getItem (" isShowSettingConfig" );
147265 const isReadonlyBefore = localStorage .getItem (" isReadonlyBefore" );
148266
@@ -158,6 +276,8 @@ export default {
158276 browserAPI .storage ? .local .get ([" clickOpenTarget" ], (res ) => {
159277 if (res && res .clickOpenTarget ) this .clickTarget = res .clickOpenTarget ;
160278 });
279+
280+ await this .loadAuthorizedSites ();
161281 },
162282};
163283< / script>
@@ -193,7 +313,8 @@ export default {
193313 }
194314}
195315
196- .CompatibilityReminder {
316+ .CompatibilityReminder ,
317+ .site - access {
197318 color: #333 ;
198319 font- size: 13px ;
199320 margin- top: 10px ;
@@ -206,4 +327,92 @@ export default {
206327 text- decoration: underline;
207328 }
208329}
330+
331+ .site - access {
332+ background: #f7f8fa;
333+
334+ .site - access- title {
335+ font- size: 14px ;
336+ font- weight: 600 ;
337+ margin- bottom: 8px ;
338+ }
339+
340+ .site - access- desc,
341+ .site - access- tip {
342+ line- height: 1.6 ;
343+ opacity: 0.8 ;
344+ }
345+
346+ .site - access- group + .site - access- group,
347+ .site - access- form,
348+ .site - access- tip {
349+ margin- top: 12px ;
350+ }
351+
352+ .group - title {
353+ font- size: 13px ;
354+ font- weight: 600 ;
355+ margin- bottom: 8px ;
356+ }
357+
358+ .site - list .tags {
359+ display: flex;
360+ flex- wrap: wrap;
361+ gap: 8px ;
362+ }
363+
364+ .site - tag {
365+ display: inline- flex;
366+ align- items: center;
367+ padding: 4px 8px ;
368+ border- radius: 999px ;
369+ background: #e8f3ff;
370+ color: #165dff ;
371+ word- break: break - all;
372+
373+ & .fixed {
374+ background: #e8ffea;
375+ color: #00a854 ;
376+ }
377+ }
378+
379+ .site - list .rows {
380+ display: flex;
381+ flex- direction: column;
382+ gap: 8px ;
383+ }
384+
385+ .site - row {
386+ display: flex;
387+ align- items: center;
388+ justify- content: space- between;
389+ gap: 10px ;
390+ padding: 8px 10px ;
391+ border- radius: 6px ;
392+ background: #fff;
393+ }
394+
395+ .site - origin {
396+ word- break: break - all;
397+ flex: 1 ;
398+ }
399+
400+ .site - empty {
401+ opacity: 0.7 ;
402+ }
403+
404+ .site - access- form {
405+ display: flex;
406+ flex- direction: column;
407+ gap: 8px ;
408+
409+ input {
410+ width: 100 % ;
411+ border: 1px solid #d9d9d9;
412+ border- radius: 6px ;
413+ padding: 8px 10px ;
414+ box- sizing: border- box;
415+ }
416+ }
417+ }
209418< / style>
0 commit comments