7979
8080 <div class =" card-footer" >
8181 <span class =" version-tag" >v{{ item.version }}</span >
82- <button class =" action-btn" @click.stop =" openGithub(item)" >
83- 查看源码
84- <svg xmlns =" http://www.w3.org/2000/svg" width =" 14" height =" 14" viewBox =" 0 0 24 24" fill =" none" stroke =" currentColor" stroke-width =" 2" stroke-linecap =" round" stroke-linejoin =" round" ><path d =" M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" ></path ><polyline points =" 15 3 21 3 21 9" ></polyline ><line x1 =" 10" y1 =" 14" x2 =" 21" y2 =" 3" ></line ></svg >
85- </button >
82+ <div class =" footer-actions" >
83+ <button class =" action-btn copy-btn" @click.stop =" handleCopy(item)" :class =" { 'copied': copiedStates[getCopyStateKey(item)] }" >
84+ <span v-if =" copiedStates[getCopyStateKey(item)]" >✓ 已复制</span >
85+ <span v-else >📋 复制指令</span >
86+ </button >
87+ <button class =" action-btn" @click.stop =" openGithub(item)" >
88+ 源码
89+ <svg xmlns =" http://www.w3.org/2000/svg" width =" 14" height =" 14" viewBox =" 0 0 24 24" fill =" none" stroke =" currentColor" stroke-width =" 2" stroke-linecap =" round" stroke-linejoin =" round" ><path d =" M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" ></path ><polyline points =" 15 3 21 3 21 9" ></polyline ><line x1 =" 10" y1 =" 14" x2 =" 21" y2 =" 3" ></line ></svg >
90+ </button >
91+ </div >
8692 </div >
8793 </div >
8894 </div >
164170 </div >
165171 </div >
166172 </Transition >
173+
174+ <!-- Install Warning Modal -->
175+ <Transition name="modal-fade">
176+ <div v-if =" warningVisible" class =" modal-overlay" @click =" warningVisible = false" >
177+ <div class =" modal-content warning-modal" @click.stop >
178+ <div class =" modal-header" >
179+ <h3 >⚠️ 安装前必读</h3 >
180+ <button class =" modal-close" @click =" warningVisible = false" >
181+ <svg width =" 24" height =" 24" viewBox =" 0 0 24 24" fill =" none" stroke =" currentColor" stroke-width =" 2" ><line x1 =" 18" y1 =" 6" x2 =" 6" y2 =" 18" ></line ><line x1 =" 6" y1 =" 6" x2 =" 18" y2 =" 18" ></line ></svg >
182+ </button >
183+ </div >
184+ <div class =" modal-body" >
185+ <p class =" warning-text" ><strong >当前 ToolDelta 框架尚未内置此功能。</strong ></p >
186+ <p class =" warning-text" >使用快捷安装指令前,请确保您已手动安装了 <strong >"插件快捷安装"</strong > 插件。</p >
187+ <div class =" command-preview" >
188+ <code >{{ getInstallCmd(pendingCmdItem) }}</code >
189+ </div >
190+ </div >
191+ <div class =" modal-footer" >
192+ <button class =" primary-btn full-width" @click =" confirmWarning" >
193+ 我知道了,复制指令
194+ </button >
195+ </div >
196+ </div >
197+ </div >
198+ </Transition >
167199 </div >
168200</template >
169201
170202<script setup>
171- import { ref , computed , onMounted , onUnmounted , watch , nextTick } from ' vue'
203+ import { ref , computed , onMounted , onUnmounted , watch , nextTick , reactive } from ' vue'
172204
173205const itemList = ref ([])
174206const loading = ref (true )
175- const pluginNameMap = ref ({})
207+ const pluginNameMap = ref ({}) // display name -> install id
208+ const pluginFolderMap = ref ({}) // display name -> folder path for GitHub/assets
176209const searchQuery = ref (' ' )
177210const modalVisible = ref (false )
178211const currentItem = ref (null )
212+ const warningVisible = ref (false )
213+ const pendingCmdItem = ref (null )
214+ const copiedStates = reactive ({}) // Use reactive for object property reactivity
179215let modalResetTimer = null
180216
181217// Minecraft Color Maps
@@ -312,6 +348,71 @@ const packageCount = computed(() => itemList.value.filter(i => i.isPackage).leng
312348
313349watch (filteredList, () => nextTick (setupCardObserver), { flush: ' post' })
314350
351+ const getInstallCmd = (item ) => {
352+ if (! item) return ' '
353+ const installId = pluginNameMap .value [item .name ]
354+ || (item .isPackage ? item .originalName : item .pluginId )
355+ || item .name
356+ return ` plg add ${ installId} `
357+ // return `plg add ${installId} -url https://pm.tooldelta.top`
358+ }
359+
360+ // Generate stable key for copiedStates to avoid collisions
361+ const getCopyStateKey = (item ) => {
362+ if (! item) return ' '
363+ const installId = pluginNameMap .value [item .name ]
364+ || (item .isPackage ? item .originalName : item .pluginId )
365+ || item .name
366+ return ` ${ item .isPackage ? ' pkg' : ' plugin' } _${ installId} `
367+ }
368+
369+ const handleCopy = (item ) => {
370+ try {
371+ if (typeof localStorage !== ' undefined' && localStorage .getItem (' td_install_cmd_warning_shown' )) {
372+ executeCopy (item)
373+ } else {
374+ pendingCmdItem .value = item
375+ warningVisible .value = true
376+ }
377+ } catch (err) {
378+ // localStorage might be disabled (Safari private mode, etc.)
379+ console .warn (' localStorage unavailable, showing warning anyway:' , err)
380+ pendingCmdItem .value = item
381+ warningVisible .value = true
382+ }
383+ }
384+
385+ const confirmWarning = () => {
386+ try {
387+ if (typeof localStorage !== ' undefined' ) {
388+ localStorage .setItem (' td_install_cmd_warning_shown' , ' true' )
389+ }
390+ } catch (err) {
391+ console .warn (' Could not save warning preference:' , err)
392+ }
393+ warningVisible .value = false
394+ if (pendingCmdItem .value ) {
395+ executeCopy (pendingCmdItem .value )
396+ pendingCmdItem .value = null
397+ }
398+ }
399+
400+ const executeCopy = async (item ) => {
401+ const cmd = getInstallCmd (item)
402+ const stateKey = getCopyStateKey (item)
403+ try {
404+ await navigator .clipboard .writeText (cmd)
405+ copiedStates[stateKey] = true
406+ setTimeout (() => {
407+ copiedStates[stateKey] = false
408+ }, 2000 )
409+ } catch (err) {
410+ console .error (' Copy failed' , err)
411+ // Fallback for older browsers
412+ alert (' 复制失败,请手动复制:' + cmd)
413+ }
414+ }
415+
315416const openGithub = (item ) => {
316417 if (! item) return
317418 const baseUrl = ' https://github.com/ToolDelta-Basic/PluginMarket/tree/main'
@@ -320,7 +421,7 @@ const openGithub = (item) => {
320421 const packageName = item .name .replace (' [整合包] ' , ' ' )
321422 path = ` /%5Bpkg%5D${ encodeURIComponent (packageName)} `
322423 } else {
323- const pluginFolderName = pluginNameMap .value [item .name ]
424+ const pluginFolderName = pluginFolderMap .value [item .name ]
324425 if (pluginFolderName) path = ` /${ encodeURIComponent (pluginFolderName)} `
325426 }
326427 if (path) window .open (baseUrl + path, ' _blank' )
@@ -354,40 +455,45 @@ onMounted(() => {
354455 return fetch (' https://pm.tooldelta.top/plugin_ids_map.json' )
355456 .then (r => r .json ())
356457 .then (pluginIdsMap => {
357- const packages = Object .entries (data .Packages ).map (([key , val ]) => ({
358- name: ` [整合包] ${ key} ` ,
359- originalName: key,
360- author: val .author ,
361- version: val .version ,
362- description: val .description ,
363- isPackage: true ,
364- includedPlugins: (val[' plugin-ids' ] || []).map (id => {
365- const p = data .MarketPlugins [id]
366- return p
367- ? { name: p .name , author: p .author , version: p .version }
368- : { name: id, author: ' 未知' , version: ' ?' }
369- })
370- }))
458+ const packages = Object .entries (data .Packages ).map (([key , val ]) => {
459+ const displayName = ` [整合包] ${ key} `
460+ pluginNameMap .value [displayName] = key
461+ return {
462+ name: displayName,
463+ originalName: key,
464+ author: val .author ,
465+ version: val .version ,
466+ description: val .description ,
467+ isPackage: true ,
468+ includedPlugins: (val[' plugin-ids' ] || []).map (id => {
469+ const p = data .MarketPlugins [id]
470+ return p
471+ ? { name: p .name , author: p .author , version: p .version }
472+ : { name: id, author: ' 未知' , version: ' ?' }
473+ })
474+ }
475+ })
371476
372477 const plugins = []
373478 const descTasks = []
374479
375480 for (const pluginId in data .MarketPlugins ) {
376481 const info = data .MarketPlugins [pluginId]
377- const name = pluginIdsMap[pluginId]
378- if (name) {
379- pluginNameMap .value [info .name ] = name
380- plugins .push ({ ... info, description: ' 加载中...' , isPackage: false })
482+ const folderName = pluginIdsMap[pluginId]
483+ if (folderName) {
484+ pluginNameMap .value [info .name ] = pluginId
485+ pluginFolderMap .value [info .name ] = folderName
486+ plugins .push ({ ... info, pluginId, description: ' 加载中...' , isPackage: false })
381487 const targetIndex = packages .length + plugins .length - 1
382- descTasks .push ({ targetIndex, pluginName : name })
488+ descTasks .push ({ targetIndex, folderName })
383489 }
384490 }
385491
386492 itemList .value = [... packages, ... plugins]
387493 loading .value = false
388494
389- descTasks .forEach (({ targetIndex, pluginName }) => {
390- fetch (` https://pm.tooldelta.top/${ pluginName } /datas.json` )
495+ descTasks .forEach (({ targetIndex, folderName }) => {
496+ fetch (` https://pm.tooldelta.top/${ folderName } /datas.json` )
391497 .then (r => r .json ())
392498 .then (d => {
393499 const next = [... itemList .value ]
@@ -638,6 +744,11 @@ onUnmounted(() => {
638744 border-radius : 0 0 12px 12px ;
639745}
640746
747+ .footer-actions {
748+ display : flex ;
749+ gap : 8px ;
750+ }
751+
641752.version-tag {
642753 font-size : 12px ;
643754 font-family : var (--vp-font-family-mono );
@@ -667,6 +778,10 @@ onUnmounted(() => {
667778 background : var (--vp-c-brand-dimm );
668779}
669780
781+ .action-btn.copied {
782+ color : var (--vp-c-green-2 , #10b981 );
783+ }
784+
670785/* Modal */
671786.modal-overlay {
672787 position : fixed ;
@@ -692,6 +807,10 @@ onUnmounted(() => {
692807 border : 1px solid var (--vp-c-divider );
693808}
694809
810+ .warning-modal {
811+ max-width : 450px ;
812+ }
813+
695814.modal-header {
696815 padding : 24px ;
697816 border-bottom : 1px solid var (--vp-c-divider );
@@ -899,6 +1018,27 @@ onUnmounted(() => {
8991018 background : var (--vp-c-brand-dark );
9001019}
9011020
1021+ .full-width {
1022+ width : 100% ;
1023+ justify-content : center ;
1024+ }
1025+
1026+ .warning-text {
1027+ margin-bottom : 12px ;
1028+ line-height : 1.6 ;
1029+ }
1030+
1031+ .command-preview {
1032+ margin-top : 16px ;
1033+ padding : 12px ;
1034+ background : var (--vp-c-bg-alt );
1035+ border-radius : 8px ;
1036+ font-family : var (--vp-font-family-mono );
1037+ font-size : 13px ;
1038+ word-break : break-all ;
1039+ border : 1px solid var (--vp-c-divider );
1040+ }
1041+
9021042/* Skeleton */
9031043.skeleton-card {
9041044 cursor : default ;
0 commit comments