Skip to content

完整示例

本页展示 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 永远不关闭!

基于 OpenSumi IDE 构建