完整示例
本页展示 Mini-SDK 各核心 API 的可交互示例。所有示例运行于模拟环境,无需连接真实设备。
🌐 网络请求
GET / POST 请求
<script setup lang="ts">
import { ref } from 'vue'
import woo from 'mini-sdk'
const status = ref('')
const response = ref<any>(null)
const loading = ref(false)
async function doGet() {
loading.value = true
status.value = '请求中...'
response.value = null
try {
const res = await woo.get('https://api.example.com/user')
status.value = `✅ 成功 (HTTP ${res.statusCode})`
response.value = res.data
} catch (err: any) {
status.value = `❌ 失败: ${err.message}`
} finally {
loading.value = false
}
}
async function doPost() {
loading.value = true
status.value = '提交中...'
response.value = null
try {
const res = await woo.post('https://api.example.com/items', {
name: '新建项目',
category: 'tool',
})
status.value = `✅ 创建成功 (HTTP ${res.statusCode})`
response.value = res.data
} catch (err: any) {
status.value = `❌ 失败: ${err.message}`
} finally {
loading.value = false
}
}
</script>
<template>
<div class="demo-network">
<div class="btn-row">
<button class="btn btn-primary" :disabled="loading" @click="doGet">
<span v-if="loading && status.includes('请求')" class="spinner" />
GET /user
</button>
<button class="btn btn-secondary" :disabled="loading" @click="doPost">
<span v-if="loading && status.includes('提交')" class="spinner" />
POST /items
</button>
</div>
<div v-if="status" class="result-box">
<div class="status-label">{{ status }}</div>
<pre v-if="response" class="response-json">{{ JSON.stringify(response, null, 2) }}</pre>
</div>
</div>
</template>
<style scoped>
.demo-network { display: flex; flex-direction: column; gap: 16px; width: 100%; }
.btn-row { display: flex; gap: 10px; flex-wrap: wrap; }
.btn {
display: inline-flex; align-items: center; gap: 6px;
padding: 8px 18px; border-radius: 7px; border: none;
font-size: 13.5px; font-weight: 500; cursor: pointer;
transition: all 0.15s ease; font-family: inherit;
}
.btn:disabled { opacity: 0.6; cursor: not-allowed; }
.btn-primary { background: #6366f1; color: white; }
.btn-primary:hover:not(:disabled) { background: #4f46e5; }
.btn-secondary { background: #f1f5f9; color: #334155; border: 1px solid #e2e8f0; }
.btn-secondary:hover:not(:disabled) { background: #e2e8f0; }
.spinner {
width: 12px; height: 12px; border: 2px solid currentColor;
border-top-color: transparent; border-radius: 50%;
animation: spin 0.6s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
.result-box { background: var(--vp-c-bg-soft); border: 1px solid var(--vp-c-divider); border-radius: 8px; padding: 12px 16px; }
.status-label { font-size: 13px; font-weight: 500; margin-bottom: 8px; color: var(--vp-c-text-1); }
.response-json { margin: 0; font-size: 12px; color: var(--vp-c-text-2); white-space: pre-wrap; }
</style>
文件上传(带进度)
<script setup lang="ts">
import { ref } from 'vue'
import woo from 'mini-sdk'
const progress = ref(0)
const status = ref('')
const uploadedUrl = ref('')
const isUploading = ref(false)
async function pickAndUpload() {
isUploading.value = false
status.value = ''
uploadedUrl.value = ''
progress.value = 0
try {
// Step 1: choose image
status.value = '📂 选择图片中...'
const { tempFilePaths } = await woo.chooseImage({ count: 1, sourceType: ['album'] })
// Step 2: simulate upload progress
isUploading.value = true
status.value = '⬆️ 上传中...'
// Mock progress bar
const interval = setInterval(() => {
progress.value = Math.min(progress.value + Math.random() * 25, 95)
}, 300)
const uploadRes = await woo.uploadFile({
url: 'https://api.example.com/upload',
filePath: tempFilePaths[0],
name: 'file',
formData: { type: 'avatar' },
})
clearInterval(interval)
progress.value = 100
const serverRes = JSON.parse(uploadRes.data)
uploadedUrl.value = serverRes.url
status.value = '✅ 上传成功'
} catch (err: any) {
status.value = `❌ 失败: ${err?.message ?? '用户取消'}`
} finally {
isUploading.value = false
}
}
</script>
<template>
<div class="demo-upload">
<button class="btn" :disabled="isUploading" @click="pickAndUpload">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="16 16 12 12 8 16"/><line x1="12" y1="12" x2="12" y2="21"/><path d="M20.39 18.39A5 5 0 0 0 18 9h-1.26A8 8 0 1 0 3 16.3"/></svg>
选择图片并上传
</button>
<div v-if="isUploading || progress > 0" class="progress-wrap">
<div class="progress-bar">
<div class="progress-fill" :style="{ width: progress + '%' }" />
</div>
<span class="progress-text">{{ Math.round(progress) }}%</span>
</div>
<div v-if="status" class="status-row">{{ status }}</div>
<div v-if="uploadedUrl" class="url-box">
<span class="url-label">服务器地址:</span>
<code>{{ uploadedUrl }}</code>
</div>
</div>
</template>
<style scoped>
.demo-upload { display: flex; flex-direction: column; gap: 14px; width: 100%; }
.btn {
display: inline-flex; align-items: center; gap: 7px;
padding: 8px 18px; border-radius: 7px; background: #6366f1; color: white;
border: none; font-size: 13.5px; font-weight: 500; cursor: pointer;
transition: background 0.15s; font-family: inherit; width: fit-content;
}
.btn:hover:not(:disabled) { background: #4f46e5; }
.btn:disabled { opacity: 0.6; cursor: not-allowed; }
.progress-wrap { display: flex; align-items: center; gap: 10px; }
.progress-bar { flex: 1; height: 6px; background: var(--vp-c-divider); border-radius: 99px; overflow: hidden; }
.progress-fill { height: 100%; background: linear-gradient(90deg, #6366f1, #8b5cf6); border-radius: 99px; transition: width 0.3s ease; }
.progress-text { font-size: 12px; color: var(--vp-c-text-2); min-width: 32px; text-align: right; }
.status-row { font-size: 13px; color: var(--vp-c-text-2); }
.url-box { font-size: 12.5px; background: var(--vp-c-bg-soft); border: 1px solid var(--vp-c-divider); border-radius: 6px; padding: 8px 12px; }
.url-label { color: var(--vp-c-text-3); }
code { font-family: var(--vp-font-family-mono); color: #6366f1; }
</style>
🎛️ 界面控件
showToast()
<script setup lang="ts">
import { ref } from 'vue'
import woo from 'mini-sdk'
type ToastIcon = 'success' | 'error' | 'loading' | 'none'
const duration = ref(2000)
const title = ref('操作成功')
const icon = ref<ToastIcon>('success')
const isLoading = ref(false)
async function showToast() {
await woo.showToast({ title: title.value, icon: icon.value, duration: duration.value === 0 ? 0 : duration.value })
if (icon.value === 'loading' || duration.value === 0) {
isLoading.value = true
} else {
isLoading.value = false
}
}
async function hideToast() {
await woo.hideToast()
isLoading.value = false
}
const iconOptions: { label: string; value: ToastIcon }[] = [
{ label: '✅ success', value: 'success' },
{ label: '❌ error', value: 'error' },
{ label: '⏳ loading', value: 'loading' },
{ label: '📝 none', value: 'none' },
]
</script>
<template>
<div class="demo-toast">
<div class="form-grid">
<div class="field">
<label>标题文字</label>
<input v-model="title" placeholder="输入提示文字" />
</div>
<div class="field">
<label>图标 icon</label>
<select v-model="icon">
<option v-for="opt in iconOptions" :key="opt.value" :value="opt.value">{{ opt.label }}</option>
</select>
</div>
<div class="field">
<label>显示时长 (ms,0=永久)</label>
<input v-model.number="duration" type="number" min="0" step="500" />
</div>
</div>
<div class="btn-row">
<button class="btn btn-primary" @click="showToast">showToast()</button>
<button class="btn btn-outline" @click="hideToast" :disabled="!isLoading">hideToast()</button>
</div>
<div class="note" v-if="isLoading">
↑ Toast 已锁定显示(duration=0 或 loading 图标),点击 hideToast 关闭
</div>
</div>
</template>
<style scoped>
.demo-toast { display: flex; flex-direction: column; gap: 14px; width: 100%; }
.form-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 12px; }
.field { display: flex; flex-direction: column; gap: 5px; }
label { font-size: 11.5px; font-weight: 600; color: var(--vp-c-text-3); text-transform: uppercase; letter-spacing: 0.05em; }
input, select {
padding: 7px 10px; border-radius: 6px; border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg); font-size: 13px; color: var(--vp-c-text-1);
font-family: inherit; outline: none; transition: border-color 0.15s;
}
input:focus, select:focus { border-color: #6366f1; }
.btn-row { display: flex; gap: 8px; }
.btn { padding: 8px 18px; border-radius: 7px; border: none; font-size: 13.5px; font-weight: 500; cursor: pointer; font-family: inherit; transition: all 0.15s; }
.btn-primary { background: #6366f1; color: white; }
.btn-primary:hover { background: #4f46e5; }
.btn-outline { background: var(--vp-c-bg); border: 1px solid var(--vp-c-divider); color: var(--vp-c-text-1); }
.btn-outline:hover:not(:disabled) { background: var(--vp-c-bg-soft); }
.btn-outline:disabled { opacity: 0.5; cursor: not-allowed; }
.note { font-size: 12.5px; color: #f59e0b; background: rgba(245,158,11,0.08); border: 1px solid rgba(245,158,11,0.2); border-radius: 6px; padding: 8px 12px; }
</style>
showDialog() / alert()
<script setup lang="ts">
import { ref } from 'vue'
import woo from 'mini-sdk'
const result = ref<{ confirm: boolean; cancel: boolean } | null>(null)
const showCancel = ref(true)
const confirmText = ref('确认')
const cancelText = ref('取消')
const content = ref('此操作不可撤销,是否继续?')
async function openDialog() {
result.value = null
const res = await woo.showDialog({
title: '确认操作',
content: content.value,
showCancel: showCancel.value,
confirmText: confirmText.value,
cancelText: cancelText.value,
})
result.value = res
}
async function openAlert() {
await woo.alert({ title: '温馨提示', content: '操作已完成!', buttonText: '知道了' })
}
</script>
<template>
<div class="demo-dialog">
<div class="form-grid">
<div class="field flex-2">
<label>内容文字</label>
<input v-model="content" />
</div>
<div class="field">
<label>确认按钮文字</label>
<input v-model="confirmText" />
</div>
<div class="field">
<label>取消按钮文字</label>
<input v-model="cancelText" />
</div>
<div class="field checkbox-field">
<label class="checkbox-label">
<input type="checkbox" v-model="showCancel" />
显示取消按钮
</label>
</div>
</div>
<div class="btn-row">
<button class="btn btn-primary" @click="openDialog">showDialog()</button>
<button class="btn btn-outline" @click="openAlert">alert()</button>
</div>
<div v-if="result" class="result-chip" :class="result.confirm ? 'confirmed' : 'cancelled'">
用户点击了:{{ result.confirm ? `"${confirmText}"(confirm)` : `"${cancelText}"(cancel)` }}
</div>
</div>
</template>
<style scoped>
.demo-dialog { display: flex; flex-direction: column; gap: 14px; width: 100%; }
.form-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 12px; align-items: end; }
.flex-2 { grid-column: span 2; }
.field { display: flex; flex-direction: column; gap: 5px; }
label { font-size: 11.5px; font-weight: 600; color: var(--vp-c-text-3); text-transform: uppercase; letter-spacing: 0.05em; }
input[type="text"], input:not([type]) {
padding: 7px 10px; border-radius: 6px; border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg); font-size: 13px; color: var(--vp-c-text-1);
font-family: inherit; outline: none; transition: border-color 0.15s;
}
input:focus { border-color: #6366f1; }
.checkbox-field { justify-content: flex-end; padding-bottom: 2px; }
.checkbox-label { display: flex; align-items: center; gap: 7px; font-size: 13px; font-weight: 500; color: var(--vp-c-text-1); text-transform: none; letter-spacing: 0; cursor: pointer; }
.checkbox-label input { width: 15px; height: 15px; cursor: pointer; }
.btn-row { display: flex; gap: 8px; }
.btn { padding: 8px 18px; border-radius: 7px; border: none; font-size: 13.5px; font-weight: 500; cursor: pointer; font-family: inherit; transition: all 0.15s; }
.btn-primary { background: #6366f1; color: white; }
.btn-primary:hover { background: #4f46e5; }
.btn-outline { background: var(--vp-c-bg); border: 1px solid var(--vp-c-divider); color: var(--vp-c-text-1); }
.btn-outline:hover { background: var(--vp-c-bg-soft); }
.result-chip { display: inline-flex; align-items: center; padding: 7px 14px; border-radius: 7px; font-size: 13px; font-weight: 500; }
.confirmed { background: rgba(16,185,129,0.1); color: #059669; border: 1px solid rgba(16,185,129,0.2); }
.cancelled { background: rgba(239,68,68,0.08); color: #dc2626; border: 1px solid rgba(239,68,68,0.15); }
</style>
💾 本地存储
Storage CRUD
<script setup lang="ts">
import { ref } from 'vue'
import woo from 'mini-sdk'
const key = ref('userProfile')
const value = ref('{"name":"Alice","theme":"dark"}')
const retrieved = ref<any>(null)
const log = ref<string[]>([])
function addLog(msg: string) {
log.value.unshift(`[${new Date().toLocaleTimeString()}] ${msg}`)
if (log.value.length > 5) log.value.pop()
}
async function doSet() {
try {
const data = JSON.parse(value.value)
await woo.setStorage({ key: key.value, data })
addLog(`✅ setStorage("${key.value}") 成功`)
} catch {
await woo.setStorage({ key: key.value, data: value.value })
addLog(`✅ setStorage("${key.value}") 已存字符串`)
}
}
async function doGet() {
const res = await woo.getStorage({ key: key.value })
retrieved.value = res.data
if (res.data === null) {
addLog(`⚠️ getStorage("${key.value}") → null(key 不存在)`)
} else {
addLog(`✅ getStorage("${key.value}") 成功`)
}
}
async function doRemove() {
await woo.removeStorage({ key: key.value })
retrieved.value = null
addLog(`🗑️ removeStorage("${key.value}") 成功`)
}
async function doClear() {
await woo.clearStorage()
retrieved.value = null
addLog('🧹 clearStorage() — 所有数据已清除')
}
</script>
<template>
<div class="demo-storage">
<div class="field-row">
<div class="field">
<label>Key</label>
<input v-model="key" placeholder="key" />
</div>
<div class="field flex-1">
<label>Value (JSON 或字符串)</label>
<input v-model="value" placeholder='{"name":"Alice"}' />
</div>
</div>
<div class="btn-row">
<button class="btn btn-primary" @click="doSet">setStorage</button>
<button class="btn btn-outline" @click="doGet">getStorage</button>
<button class="btn btn-outline" @click="doRemove">removeStorage</button>
<button class="btn btn-danger" @click="doClear">clearStorage</button>
</div>
<div v-if="retrieved !== null" class="result-box">
<div class="result-label">getStorage 返回值:</div>
<pre class="result-content">{{ JSON.stringify(retrieved, null, 2) }}</pre>
</div>
<div v-if="log.length" class="log-box">
<div v-for="(entry, i) in log" :key="i" class="log-entry">{{ entry }}</div>
</div>
</div>
</template>
<style scoped>
.demo-storage { display: flex; flex-direction: column; gap: 14px; width: 100%; }
.field-row { display: flex; gap: 10px; flex-wrap: wrap; }
.field { display: flex; flex-direction: column; gap: 5px; }
.flex-1 { flex: 1; min-width: 180px; }
label { font-size: 11.5px; font-weight: 600; color: var(--vp-c-text-3); text-transform: uppercase; letter-spacing: 0.05em; }
input {
padding: 7px 10px; border-radius: 6px;
border: 1px solid var(--vp-c-divider); background: var(--vp-c-bg);
font-size: 13px; color: var(--vp-c-text-1); font-family: var(--vp-font-family-mono);
outline: none; transition: border-color 0.15s;
}
input:focus { border-color: #6366f1; }
.btn-row { display: flex; gap: 8px; flex-wrap: wrap; }
.btn { padding: 7px 14px; border-radius: 6px; border: none; font-size: 13px; font-weight: 500; cursor: pointer; font-family: inherit; transition: all 0.15s; }
.btn-primary { background: #6366f1; color: white; }
.btn-primary:hover { background: #4f46e5; }
.btn-outline { background: var(--vp-c-bg); border: 1px solid var(--vp-c-divider); color: var(--vp-c-text-1); }
.btn-outline:hover { background: var(--vp-c-bg-soft); }
.btn-danger { background: #fee2e2; color: #dc2626; }
.btn-danger:hover { background: #fecaca; }
.result-box { background: var(--vp-c-bg-soft); border: 1px solid var(--vp-c-divider); border-radius: 8px; padding: 12px 14px; }
.result-label { font-size: 11.5px; font-weight: 600; color: var(--vp-c-text-3); text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 8px; }
.result-content { margin: 0; font-size: 12px; color: var(--vp-c-text-2); font-family: var(--vp-font-family-mono); white-space: pre-wrap; }
.log-box { border: 1px solid var(--vp-c-divider); border-radius: 8px; overflow: hidden; }
.log-entry { padding: 7px 12px; font-size: 12px; color: var(--vp-c-text-2); font-family: var(--vp-font-family-mono); border-bottom: 1px solid var(--vp-c-divider); }
.log-entry:last-child { border-bottom: none; }
</style>
🗺️ 路由导航
Router API
<script setup lang="ts">
import { ref } from 'vue'
import woo from 'mini-sdk'
const url = ref('pages/detail/index')
const params = ref('id=42&type=article')
const log = ref<string[]>([])
function addLog(msg: string) {
log.value.unshift(`[${new Date().toLocaleTimeString()}] ${msg}`)
if (log.value.length > 6) log.value.pop()
}
async function doNavigateTo() {
const query = Object.fromEntries(new URLSearchParams(params.value))
await woo.navigateTo({ url: url.value, params: query })
addLog(`navigateTo("${url.value}", ${JSON.stringify(query)})`)
}
async function doNavigateBack() {
await woo.navigateBack()
addLog('navigateBack()')
}
async function doReLaunch() {
await woo.reLaunch(url.value)
addLog(`reLaunch("${url.value}")`)
}
async function doGetRoute() {
const route = woo.getRoute()
addLog(`getRoute() → path="${route.path}" query=${JSON.stringify(route.query)}`)
}
async function doGetPages() {
const res = await woo.getCurrentPages()
addLog(`getCurrentPages() → ${JSON.stringify(res.pages)}`)
}
</script>
<template>
<div class="demo-router">
<div class="form-grid">
<div class="field flex-2">
<label>目标页面路径</label>
<input v-model="url" placeholder="pages/detail/index" />
</div>
<div class="field flex-2">
<label>参数 (query string 格式)</label>
<input v-model="params" placeholder="id=42&type=article" />
</div>
</div>
<div class="btn-row">
<button class="btn btn-primary" @click="doNavigateTo">navigateTo</button>
<button class="btn btn-outline" @click="doNavigateBack">navigateBack</button>
<button class="btn btn-outline" @click="doReLaunch">reLaunch</button>
<button class="btn btn-ghost" @click="doGetRoute">getRoute</button>
<button class="btn btn-ghost" @click="doGetPages">getCurrentPages</button>
</div>
<div v-if="log.length" class="log-box">
<div v-for="(entry, i) in log" :key="i" class="log-entry">{{ entry }}</div>
</div>
</div>
</template>
<style scoped>
.demo-router { display: flex; flex-direction: column; gap: 14px; width: 100%; }
.form-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 12px; }
.flex-2 { grid-column: span 2; }
.field { display: flex; flex-direction: column; gap: 5px; }
label { font-size: 11.5px; font-weight: 600; color: var(--vp-c-text-3); text-transform: uppercase; letter-spacing: 0.05em; }
input { padding: 7px 10px; border-radius: 6px; border: 1px solid var(--vp-c-divider); background: var(--vp-c-bg); font-size: 13px; color: var(--vp-c-text-1); font-family: var(--vp-font-family-mono); outline: none; transition: border-color 0.15s; }
input:focus { border-color: #6366f1; }
.btn-row { display: flex; gap: 8px; flex-wrap: wrap; }
.btn { padding: 7px 14px; border-radius: 6px; border: none; font-size: 13px; font-weight: 500; cursor: pointer; font-family: inherit; transition: all 0.15s; }
.btn-primary { background: #6366f1; color: white; }
.btn-primary:hover { background: #4f46e5; }
.btn-outline { background: var(--vp-c-bg); border: 1px solid var(--vp-c-divider); color: var(--vp-c-text-1); }
.btn-outline:hover { background: var(--vp-c-bg-soft); }
.btn-ghost { background: transparent; color: var(--vp-c-text-2); border: 1px dashed var(--vp-c-divider); }
.btn-ghost:hover { background: var(--vp-c-bg-soft); color: var(--vp-c-text-1); }
.log-box { border: 1px solid var(--vp-c-divider); border-radius: 8px; overflow: hidden; max-height: 180px; overflow-y: auto; }
.log-entry { padding: 7px 12px; font-size: 12px; color: var(--vp-c-text-2); font-family: var(--vp-font-family-mono); border-bottom: 1px solid var(--vp-c-divider); white-space: nowrap; }
.log-entry:last-child { border-bottom: none; }
</style>
📱 设备信息
Device API
<script setup lang="ts">
import { ref } from 'vue'
import woo from 'mini-sdk'
const systemInfo = ref<any>(null)
const deviceInfo = ref<any>(null)
const networkType = ref<string | null>(null)
const loading = ref(false)
async function getSystemInfo() {
loading.value = true
systemInfo.value = await woo.getSystemInfo()
loading.value = false
}
async function getDeviceInfo() {
loading.value = true
deviceInfo.value = await woo.getDeviceInfo()
loading.value = false
}
async function getNetwork() {
const { networkType: nt } = await woo.getNetworkType()
networkType.value = nt
}
async function vibrateShort() {
await woo.vibrateShort()
}
</script>
<template>
<div class="demo-device">
<div class="btn-row">
<button class="btn btn-primary" :disabled="loading" @click="getSystemInfo">getSystemInfo()</button>
<button class="btn btn-outline" :disabled="loading" @click="getDeviceInfo">getDeviceInfo()</button>
<button class="btn btn-outline" @click="getNetwork">getNetworkType()</button>
<button class="btn btn-ghost" @click="vibrateShort">vibrateShort()</button>
</div>
<div v-if="networkType !== null" class="info-chip">
网络类型:<strong>{{ networkType }}</strong>
</div>
<div v-if="systemInfo" class="info-grid">
<div class="info-title">SystemInfo</div>
<div v-for="(val, key) in systemInfo" :key="key" class="info-row">
<span class="info-key">{{ key }}</span>
<span class="info-val">{{ val }}</span>
</div>
</div>
<div v-if="deviceInfo" class="info-grid">
<div class="info-title">DeviceInfo</div>
<div v-for="(val, key) in deviceInfo" :key="key" class="info-row">
<span class="info-key">{{ key }}</span>
<span class="info-val">{{ val }}</span>
</div>
</div>
</div>
</template>
<style scoped>
.demo-device { display: flex; flex-direction: column; gap: 14px; width: 100%; }
.btn-row { display: flex; gap: 8px; flex-wrap: wrap; }
.btn { padding: 7px 14px; border-radius: 6px; border: none; font-size: 13px; font-weight: 500; cursor: pointer; font-family: inherit; transition: all 0.15s; }
.btn:disabled { opacity: 0.6; cursor: not-allowed; }
.btn-primary { background: #6366f1; color: white; }
.btn-primary:hover:not(:disabled) { background: #4f46e5; }
.btn-outline { background: var(--vp-c-bg); border: 1px solid var(--vp-c-divider); color: var(--vp-c-text-1); }
.btn-outline:hover:not(:disabled) { background: var(--vp-c-bg-soft); }
.btn-ghost { background: transparent; color: var(--vp-c-text-2); border: 1px dashed var(--vp-c-divider); }
.btn-ghost:hover { background: var(--vp-c-bg-soft); }
.info-chip { display: inline-flex; align-items: center; gap: 6px; padding: 6px 12px; background: rgba(99,102,241,0.08); border: 1px solid rgba(99,102,241,0.2); border-radius: 6px; font-size: 13px; color: #6366f1; }
.info-grid { border: 1px solid var(--vp-c-divider); border-radius: 8px; overflow: hidden; }
.info-title { padding: 8px 14px; font-size: 11px; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; color: var(--vp-c-text-3); background: var(--vp-c-bg-soft); border-bottom: 1px solid var(--vp-c-divider); }
.info-row { display: flex; padding: 7px 14px; border-bottom: 1px solid var(--vp-c-divider); font-size: 12.5px; }
.info-row:last-child { border-bottom: none; }
.info-key { width: 140px; flex-shrink: 0; color: var(--vp-c-text-3); font-family: var(--vp-font-family-mono); }
.info-val { color: var(--vp-c-text-1); font-family: var(--vp-font-family-mono); }
</style>
📋 最佳实践速查
ts
// ✅ 推荐:使用 async/await + try/catch
try {
const res = await woo.get('https://api.example.com/data')
console.log(res.data)
} catch (err) {
if (err.code === BridgeErrorCode.NetworkUnavailable) {
await woo.showToast({ title: '网络不可用', icon: 'error' })
}
}
// ✅ 推荐:页面生命周期记得取消订阅
const offShow = woo.onShow(() => { /* ... */ })
onUnmounted(() => offShow())
// ✅ 推荐:用 finally 确保 hideLoading
try {
await woo.showLoading('加载中...')
await fetchData()
} finally {
await woo.hideLoading()
}
// ❌ 避免:忘记 hideLoading / hideToast
await woo.showLoading('加载中...') // 如果抛错,Loading 永远不关闭!