feat: 版本号按分支轨道分开管理,版本号配置收归管理员
master/develop 共用 release 轨(App_Store 打包构建号自增),feature 分支走
feature 轨(构建号永不自增,始终使用手填值),两条轨道的 App_Ver 各自独立。
- versions 改为 tracks 结构,旧的单轨/单值配置在加载时自动迁移进 release 轨
- 新增 config["branch_track"] 记录分支归属,缺省按分支名推断,可在分支管理页改
- 新增 PUT /api/config/branches/track;分支名含 / 时走请求体而非路径参数
- 修复 DELETE /api/config/branches/{name} 无法删除含 / 的分支(405)
- 版本号接口不再对普通用户开放,打包设置页整页改为管理员可见
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -24,7 +24,15 @@ function mockFetch(url) {
|
||||
'/api/config/servers': { '测试环境': { api: 'https://test.com', assDom: '', universalLink: '' } },
|
||||
'/api/config/branches': ['main', 'dev'],
|
||||
'/api/config/build': { max_concurrent_builds: 2, build_dir_retention_hours: 24, build_base_dir: '/tmp' },
|
||||
'/api/config/versions': { app_ver: '2.195.0', build_ver: '2.195.0.0' },
|
||||
'/api/config/versions': {
|
||||
app_ver: '2.195.0',
|
||||
build_ver: '2.195.0.0',
|
||||
tracks: {
|
||||
release: { app_ver: '2.195.0', build_ver: '2.195.0.0', build_map: {} },
|
||||
feature: { app_ver: '2.100.0', build_ver: '2.100.0.0', build_map: {} },
|
||||
},
|
||||
branch_track: { main: 'release', dev: 'release' },
|
||||
},
|
||||
'/api/config': { apps: {}, schemes: {}, branches: ['main'] },
|
||||
}
|
||||
return Promise.resolve({
|
||||
@@ -168,20 +176,76 @@ describe('ConfigView.vue', () => {
|
||||
expect(window.alert).toHaveBeenCalledWith('设置已保存')
|
||||
})
|
||||
|
||||
it('普通账号可修改版本号但看不到打包参数', async () => {
|
||||
it('普通账号只能管理 Apps,看不到打包设置与版本号', async () => {
|
||||
const wrapper = mountConfigView({ admin: false })
|
||||
await flushPromises()
|
||||
|
||||
const menuTexts = wrapper.findAll('.sidebar-menu li').map(li => li.text())
|
||||
expect(menuTexts).toEqual(['Apps 配置', '打包设置'])
|
||||
expect(menuTexts).toEqual(['Apps 配置'])
|
||||
|
||||
expect(wrapper.text()).not.toContain('上架版本号')
|
||||
expect(wrapper.text()).not.toContain('自测版本号')
|
||||
expect(wrapper.text()).not.toContain('打包参数')
|
||||
// 版本号接口为管理员专属,普通账号不再请求
|
||||
expect(fetch).not.toHaveBeenCalledWith('/api/config/versions', expect.anything())
|
||||
expect(fetch.mock.calls.map(c => c[0])).toEqual(['/api/config/apps'])
|
||||
})
|
||||
|
||||
it('两条版本轨道各自独立展示与保存', async () => {
|
||||
const calls = []
|
||||
fetch.mockImplementation((url, opts) => {
|
||||
if (url === '/api/config/versions' && opts?.method === 'PUT') {
|
||||
calls.push(JSON.parse(opts.body))
|
||||
return Promise.resolve({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ app_ver: '2.101.0', build_ver: '2.101.0.0' }),
|
||||
})
|
||||
}
|
||||
return mockFetch(url)
|
||||
})
|
||||
window.alert = vi.fn()
|
||||
|
||||
const wrapper = mountConfigView()
|
||||
await flushPromises()
|
||||
|
||||
const buildMenu = wrapper.findAll('.sidebar-menu li').find(li => li.text() === '打包设置')
|
||||
await buildMenu.trigger('click')
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
expect(wrapper.text()).toContain('应用版本号')
|
||||
expect(wrapper.text()).not.toContain('打包参数')
|
||||
// 非管理员在打包设置页可见 App_Ver 与 Build_Ver 两个输入框
|
||||
expect(wrapper.findAll('input').length).toBe(2)
|
||||
const inputs = wrapper.findAll('input[type="text"]')
|
||||
expect(inputs[0].element.value).toBe('2.195.0') // release App_Ver
|
||||
expect(inputs[2].element.value).toBe('2.100.0') // feature App_Ver
|
||||
|
||||
await inputs[2].setValue('2.101.0')
|
||||
const saveBtn = wrapper.findAll('.btn-primary').find(b => b.text() === '保存自测版本号')
|
||||
await saveBtn.trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
expect(calls).toEqual([{ track: 'feature', app_ver: '2.101.0', build_ver: '2.101.0.0' }])
|
||||
})
|
||||
|
||||
it('分支管理可切换版本轨道', async () => {
|
||||
const calls = []
|
||||
fetch.mockImplementation((url, opts) => {
|
||||
if (url === '/api/config/branches/track' && opts?.method === 'PUT') {
|
||||
calls.push(JSON.parse(opts.body))
|
||||
return Promise.resolve({ ok: true, json: () => Promise.resolve({ message: 'ok' }) })
|
||||
}
|
||||
return mockFetch(url)
|
||||
})
|
||||
|
||||
const wrapper = mountConfigView()
|
||||
await flushPromises()
|
||||
|
||||
const branchesMenu = wrapper.findAll('.sidebar-menu li').find(li => li.text() === '分支管理')
|
||||
await branchesMenu.trigger('click')
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
const select = wrapper.find('.config-table select')
|
||||
expect(select.element.value).toBe('release')
|
||||
await select.setValue('feature')
|
||||
await flushPromises()
|
||||
|
||||
expect(calls).toEqual([{ branch: 'main', track: 'feature' }])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<li v-if="isAdmin" :class="{ active: tab === 'schemes' }" @click="tab = 'schemes'">Schemes 配置</li>
|
||||
<li v-if="isAdmin" :class="{ active: tab === 'branches' }" @click="tab = 'branches'">分支管理</li>
|
||||
<li v-if="isAdmin" :class="{ active: tab === 'upload' }" @click="tab = 'upload'">上传配置</li>
|
||||
<li :class="{ active: tab === 'build' }" @click="tab = 'build'">打包设置</li>
|
||||
<li v-if="isAdmin" :class="{ active: tab === 'build' }" @click="tab = 'build'">打包设置</li>
|
||||
<li v-if="isAdmin" :class="{ active: tab === 'json' }" @click="tab = 'json'">JSON 编辑</li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -151,7 +151,10 @@
|
||||
<!-- 分支管理 -->
|
||||
<div v-if="tab === 'branches'">
|
||||
<h2>分支管理</h2>
|
||||
<p style="color: #666; margin-bottom: 16px; font-size: 14px;">管理可打包的代码分支,打包时从对应分支目录获取源码</p>
|
||||
<p style="color: #666; margin-bottom: 16px; font-size: 14px;">
|
||||
管理可打包的代码分支,打包时从对应分支目录获取源码。
|
||||
版本轨道决定该分支使用哪一套版本号:上架(master/develop 共用,App_Store 打包构建号自增)、自测(feature 用,构建号不自增)。
|
||||
</p>
|
||||
<div style="display: flex; gap: 12px; margin-bottom: 16px;">
|
||||
<input v-model="newBranch" type="text" placeholder="输入分支名称" style="flex: 1; padding: 10px 12px; border: 1px solid #d9d9d9; border-radius: 6px; font-size: 14px;">
|
||||
<button class="btn btn-primary" style="width: auto; padding: 10px 24px;" @click="addBranch">添加分支</button>
|
||||
@@ -160,18 +163,25 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th>分支名称</th>
|
||||
<th>版本轨道</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="b in branches" :key="b">
|
||||
<td><strong>{{ b }}</strong></td>
|
||||
<td>
|
||||
<select :value="branchTrack[b] || 'feature'" style="width: auto; padding: 4px 8px;" @change="setBranchTrack(b, $event.target.value)">
|
||||
<option value="release">上架(master/develop)</option>
|
||||
<option value="feature">自测(feature)</option>
|
||||
</select>
|
||||
</td>
|
||||
<td class="action-btns">
|
||||
<button class="action-btn delete" @click="deleteBranch(b)">删除</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="!branches.length">
|
||||
<td colspan="2" style="text-align: center; color: #999;">暂无分支配置</td>
|
||||
<td colspan="3" style="text-align: center; color: #999;">暂无分支配置</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -269,21 +279,36 @@
|
||||
<div v-if="tab === 'build'">
|
||||
<h2>打包设置</h2>
|
||||
<div style="max-width: 500px;">
|
||||
<h4 class="section-title">应用版本号</h4>
|
||||
<h4 class="section-title">上架版本号(master / develop)</h4>
|
||||
<div class="form-group">
|
||||
<label>App_Ver(应用版本)</label>
|
||||
<input type="text" v-model="versions.app_ver" placeholder="2.180.0" @input="onAppVerInput">
|
||||
<div style="font-size: 12px; color: #999; margin-top: 4px;">格式:主版本.次版本.修订号</div>
|
||||
<input type="text" v-model="versions.release.app_ver" placeholder="2.180.0" @input="onAppVerInput('release')">
|
||||
<div style="font-size: 12px; color: #999; margin-top: 4px;">格式:主版本.次版本.修订号。master 与 develop 共用这一套版本号。</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Build_Ver(构建号)</label>
|
||||
<input type="text" v-model="versions.build_ver" placeholder="2.180.0.0">
|
||||
<input type="text" v-model="versions.release.build_ver" placeholder="2.180.0.0">
|
||||
<div style="font-size: 12px; color: #999; margin-top: 4px;">
|
||||
四段数字,前三位须与 App_Ver 一致。修改 App_Ver 会自动切换为该版本号已记录的构建号;
|
||||
每个版本号的构建号独立递增,App_Store 打包时自动 +1。一般无需手动修改。
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-primary" style="width: auto; padding: 10px 32px; margin-bottom: 24px;" @click="saveVersions">保存版本号</button>
|
||||
<button class="btn btn-primary" style="width: auto; padding: 10px 32px; margin-bottom: 24px;" @click="saveVersions('release')">保存上架版本号</button>
|
||||
|
||||
<h4 class="section-title">自测版本号(feature 分支)</h4>
|
||||
<div class="form-group">
|
||||
<label>App_Ver(应用版本)</label>
|
||||
<input type="text" v-model="versions.feature.app_ver" placeholder="2.180.0" @input="onAppVerInput('feature')">
|
||||
<div style="font-size: 12px; color: #999; margin-top: 4px;">与上架版本号各自独立,互不影响。</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Build_Ver(构建号)</label>
|
||||
<input type="text" v-model="versions.feature.build_ver" placeholder="2.180.0.0">
|
||||
<div style="font-size: 12px; color: #999; margin-top: 4px;">
|
||||
仅供自测,打包时<strong>不会自动递增</strong>,始终使用这里填写的值(默认 .0)。需要区分时请手动修改。
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-primary" style="width: auto; padding: 10px 32px; margin-bottom: 24px;" @click="saveVersions('feature')">保存自测版本号</button>
|
||||
|
||||
<template v-if="isAdmin">
|
||||
<h4 class="section-title">打包参数</h4>
|
||||
@@ -613,20 +638,30 @@ const servers = ref({})
|
||||
const branches = ref([])
|
||||
const newBranch = ref('')
|
||||
const buildSettings = ref({})
|
||||
const versions = ref({ app_ver: '', build_ver: '' })
|
||||
const buildMap = ref({})
|
||||
// 两条版本轨道:release(master/develop,上架用)与 feature(自测用),各自独立
|
||||
const versions = ref({
|
||||
release: { app_ver: '', build_ver: '' },
|
||||
feature: { app_ver: '', build_ver: '' },
|
||||
})
|
||||
const buildMap = ref({ release: {}, feature: {} })
|
||||
const branchTrack = ref({})
|
||||
|
||||
const applyVersions = (data) => {
|
||||
buildMap.value = data.build_map || {}
|
||||
versions.value = { app_ver: data.app_ver || '', build_ver: data.build_ver || '' }
|
||||
const tracks = data.tracks || {}
|
||||
for (const track of ['release', 'feature']) {
|
||||
const item = tracks[track] || {}
|
||||
buildMap.value[track] = item.build_map || {}
|
||||
versions.value[track] = { app_ver: item.app_ver || '', build_ver: item.build_ver || '' }
|
||||
}
|
||||
branchTrack.value = data.branch_track || {}
|
||||
}
|
||||
|
||||
// 修改 App_Ver 时,自动把 Build_Ver 切换为该版本号已记录的构建号(新版本号则为 .0)
|
||||
const onAppVerInput = () => {
|
||||
const appVer = (versions.value.app_ver || '').trim()
|
||||
const onAppVerInput = (track) => {
|
||||
const appVer = (versions.value[track].app_ver || '').trim()
|
||||
if (/^\d+\.\d+\.\d+$/.test(appVer)) {
|
||||
const n = Number(buildMap.value[appVer] || 0)
|
||||
versions.value.build_ver = `${appVer}.${n}`
|
||||
const n = Number((buildMap.value[track] || {})[appVer] || 0)
|
||||
versions.value[track].build_ver = `${appVer}.${n}`
|
||||
}
|
||||
}
|
||||
const uploadConfig = ref({ mode: 'oss', oss: {}, webdav: {}, dingtalk: {} })
|
||||
@@ -659,12 +694,9 @@ onMounted(async () => {
|
||||
|
||||
const loadData = async () => {
|
||||
if (!isAdmin.value) {
|
||||
const [appsRes, versionsRes] = await Promise.all([
|
||||
authFetch('/api/config/apps'),
|
||||
authFetch('/api/config/versions'),
|
||||
])
|
||||
// 普通用户只能管理 Apps,版本号等配置均为管理员专属
|
||||
const appsRes = await authFetch('/api/config/apps')
|
||||
if (appsRes.ok) apps.value = await appsRes.json()
|
||||
if (versionsRes.ok) applyVersions(await versionsRes.json())
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1111,6 +1143,24 @@ const addBranch = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const setBranchTrack = async (name, track) => {
|
||||
try {
|
||||
const res = await authFetch('/api/config/branches/track', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ branch: name, track }),
|
||||
})
|
||||
if (!res.ok) {
|
||||
const err = await res.json()
|
||||
throw new Error(err.detail || '修改失败')
|
||||
}
|
||||
branchTrack.value = { ...branchTrack.value, [name]: track }
|
||||
} catch (e) {
|
||||
alert('修改失败: ' + e.message)
|
||||
await loadData()
|
||||
}
|
||||
}
|
||||
|
||||
const deleteBranch = async (name) => {
|
||||
if (!confirm(`确定删除分支「${name}」?`)) return
|
||||
try {
|
||||
@@ -1132,21 +1182,25 @@ const saveBuildSettings = async () => {
|
||||
alert('设置已保存')
|
||||
}
|
||||
|
||||
const saveVersions = async () => {
|
||||
const saveVersions = async (track = 'release') => {
|
||||
try {
|
||||
const res = await authFetch('/api/config/versions', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
app_ver: versions.value.app_ver,
|
||||
build_ver: versions.value.build_ver,
|
||||
track,
|
||||
app_ver: versions.value[track].app_ver,
|
||||
build_ver: versions.value[track].build_ver,
|
||||
}),
|
||||
})
|
||||
if (res.ok) {
|
||||
const data = await res.json()
|
||||
// 回写该版本号最新的构建号,保持本地 build_map 与服务端一致
|
||||
buildMap.value = { ...buildMap.value, [data.app_ver]: Number((data.build_ver.split('.')[3]) || 0) }
|
||||
versions.value = { app_ver: data.app_ver, build_ver: data.build_ver }
|
||||
buildMap.value[track] = {
|
||||
...(buildMap.value[track] || {}),
|
||||
[data.app_ver]: Number((data.build_ver.split('.')[3]) || 0),
|
||||
}
|
||||
versions.value[track] = { app_ver: data.app_ver, build_ver: data.build_ver }
|
||||
alert('版本号已保存')
|
||||
} else {
|
||||
const err = await res.json()
|
||||
|
||||
Reference in New Issue
Block a user