| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293 |
- <template>
- <section class="page stack">
- <div class="panel">
- <div class="section-head">
- <div>
- <p class="section-tag">业务模块</p>
- <h2>版本管理</h2>
- </div>
- <button class="secondary" @click="loadAll">刷新</button>
- </div>
- <div v-for="item in applications" :key="item.application.id" class="app-card">
- <div class="app-card__top">
- <div>
- <h3>{{ item.application.name }}</h3>
- <p class="muted">{{ item.application.packageName }}</p>
- <p class="chips">
- <span v-for="module in item.application.businessModules" :key="module">{{ module }}</span>
- </p>
- </div>
- <label class="toggle">
- <input
- type="checkbox"
- :checked="item.application.pluginManagementEnabled"
- @change="togglePlugin(item.application.id, ($event.target as HTMLInputElement).checked)"
- />
- 插件化管理
- </label>
- </div>
- <form class="form-grid" @submit.prevent="upload(item.application.id)">
- <label>
- 包类型
- <select v-model="uploadForms[item.application.id].packageType">
- <option value="APP">App</option>
- <option value="PLUGIN">插件</option>
- </select>
- </label>
- <label>
- 版本号
- <input v-model="uploadForms[item.application.id].versionName" placeholder="0.3.0" required />
- </label>
- <label>
- 版本码
- <input v-model.number="uploadForms[item.application.id].versionCode" type="number" min="1" required />
- </label>
- <label>
- 包文件名
- <input v-model="uploadForms[item.application.id].uploadedFileName" placeholder="app-release.apk" required />
- </label>
- <label class="full">
- 下载地址
- <input v-model="uploadForms[item.application.id].downloadUrl" placeholder="https://example.com/app.apk" required />
- </label>
- <label class="full">
- 更新标题
- <input v-model="uploadForms[item.application.id].title" placeholder="发现新版本" required />
- </label>
- <label class="full">
- 更新说明
- <textarea v-model="uploadForms[item.application.id].changelog" rows="3"></textarea>
- </label>
- <button class="primary">上传版本包</button>
- </form>
- <table class="table">
- <thead>
- <tr>
- <th>版本</th>
- <th>类型</th>
- <th>状态</th>
- <th>策略</th>
- <th>灰度目标</th>
- <th></th>
- </tr>
- </thead>
- <tbody>
- <tr v-for="release in item.releases" :key="release.id">
- <td>{{ release.versionName }} ({{ release.versionCode }})</td>
- <td>{{ release.packageType }}</td>
- <td>{{ release.status }}</td>
- <td>{{ release.publishStrategy }}</td>
- <td>{{ graySummary(release) }}</td>
- <td class="actions">
- <button class="ghost" @click="publishFull(item.application.id, release.id)">全量发布</button>
- <button class="ghost" @click="selectGrayRelease(item.application.id, release.id)">灰度发布</button>
- </td>
- </tr>
- </tbody>
- </table>
- </div>
- </div>
- <div class="panel">
- <div class="section-head">
- <div>
- <p class="section-tag">灰度发布</p>
- <h2>用户平台 Hook 选人</h2>
- </div>
- <p class="muted">用户数据已脱敏,可按分组、快速选择或单个用户圈定。</p>
- </div>
- <div class="filters">
- <label>
- 分组
- <select v-model="filters.groupCode" @change="loadUsers">
- <option value="">全部</option>
- <option v-for="group in groups" :key="group.code" :value="group.code">
- {{ group.name }} ({{ group.userCount }})
- </option>
- </select>
- </label>
- <label>
- 快速选择
- <select v-model="filters.quickSelectionCode" @change="loadUsers">
- <option value="">全部</option>
- <option v-for="item in quickSelections" :key="item.code" :value="item.code">
- {{ item.name }} ({{ item.userCount }})
- </option>
- </select>
- </label>
- <label class="grow">
- 搜索
- <input v-model="filters.keyword" placeholder="用户 ID / 昵称 / 地区" @input="loadUsers" />
- </label>
- </div>
- <div class="selected-bar">
- <span>当前灰度版本:{{ selectedReleaseId || '请先点一个版本的灰度发布' }}</span>
- <span>已选择用户:{{ selectedUsers.length }}</span>
- <button class="primary" :disabled="!selectedReleaseId" @click="publishGray">确认灰度发布</button>
- </div>
- <div class="chips">
- <button
- v-for="item in quickSelections"
- :key="item.code"
- class="chip-button"
- @click="applyQuickSelection(item.code)"
- >
- {{ item.name }}
- </button>
- </div>
- <table class="table">
- <thead>
- <tr>
- <th></th>
- <th>用户 ID</th>
- <th>昵称</th>
- <th>手机号</th>
- <th>邮箱</th>
- <th>地区</th>
- <th>分组</th>
- </tr>
- </thead>
- <tbody>
- <tr v-for="user in users" :key="user.id">
- <td>
- <input type="checkbox" :checked="selectedUsers.includes(user.id)" @change="toggleUser(user.id)" />
- </td>
- <td>{{ user.id }}</td>
- <td>{{ user.nickname }}</td>
- <td>{{ user.phone }}</td>
- <td>{{ user.email }}</td>
- <td>{{ user.region }}</td>
- <td>{{ user.groupName }}</td>
- </tr>
- </tbody>
- </table>
- </div>
- </section>
- </template>
- <script setup lang="ts">
- import { onMounted, reactive, ref } from 'vue'
- import { api, type ApplicationDetail, type AudienceGroup, type AudienceUser, type QuickSelection, type ReleaseRecord } from '../api/client'
- const applications = ref<ApplicationDetail[]>([])
- const groups = ref<AudienceGroup[]>([])
- const quickSelections = ref<QuickSelection[]>([])
- const users = ref<AudienceUser[]>([])
- const selectedUsers = ref<string[]>([])
- const selectedAppId = ref('')
- const selectedReleaseId = ref('')
- const filters = reactive({
- keyword: '',
- groupCode: '',
- quickSelectionCode: '',
- })
- const uploadForms = reactive<Record<string, {
- packageType: 'APP' | 'PLUGIN'
- versionCode: number
- versionName: string
- title: string
- changelog: string
- downloadUrl: string
- uploadedFileName: string
- entryActivity: string
- forceUpdate: boolean
- }>>({})
- function ensureForm(appId: string) {
- if (!uploadForms[appId]) {
- uploadForms[appId] = {
- packageType: 'APP',
- versionCode: 1,
- versionName: '',
- title: '发现新版本',
- changelog: '',
- downloadUrl: '',
- uploadedFileName: '',
- entryActivity: 'com.xuqm.plugin.ui.PluginUiActivity',
- forceUpdate: false,
- }
- }
- }
- function graySummary(release: ReleaseRecord) {
- if (!release.grayRule) return '全量'
- const rule = release.grayRule
- return `组 ${rule.groupCodes.length} / 快选 ${rule.quickSelectionCodes.length} / 用户 ${rule.userIds.length}`
- }
- function selectGrayRelease(appId: string, releaseId: string) {
- selectedAppId.value = appId
- selectedReleaseId.value = releaseId
- }
- async function loadAll() {
- const [appList, groupList, quickList] = await Promise.all([
- api.listApplications(),
- api.listAudienceGroups(),
- api.listQuickSelections(),
- ])
- applications.value = appList
- groups.value = groupList
- quickSelections.value = quickList
- appList.forEach(item => ensureForm(item.application.id))
- await loadUsers()
- }
- async function loadUsers() {
- users.value = await api.listAudienceUsers(filters)
- }
- async function togglePlugin(appId: string, enabled: boolean) {
- await api.togglePluginManagement(appId, enabled)
- await loadAll()
- }
- async function upload(appId: string) {
- const form = uploadForms[appId]
- await api.uploadRelease(appId, form)
- await loadAll()
- }
- async function publishFull(appId: string, releaseId: string) {
- await api.publishRelease(appId, releaseId, { grayPublish: false })
- await loadAll()
- }
- function toggleUser(userId: string) {
- if (selectedUsers.value.includes(userId)) {
- selectedUsers.value = selectedUsers.value.filter(item => item !== userId)
- } else {
- selectedUsers.value = [...selectedUsers.value, userId]
- }
- }
- function applyQuickSelection(code: string) {
- filters.quickSelectionCode = code
- void loadUsers()
- }
- async function publishGray() {
- if (!selectedReleaseId.value || !selectedAppId.value) return
- await api.publishRelease(selectedAppId.value, selectedReleaseId.value, {
- grayPublish: true,
- hookName: 'user-platform-gray-hook',
- groupCodes: filters.groupCode ? [filters.groupCode] : [],
- quickSelectionCodes: filters.quickSelectionCode ? [filters.quickSelectionCode] : [],
- userIds: selectedUsers.value,
- })
- await loadAll()
- }
- onMounted(() => {
- void loadAll()
- })
- </script>
|