feat: 新增改枪码录入页,修复深链接与标签栏刷新回首页问题
CI/CD Deploy (Web) / 构建前端镜像并部署 (push) Successful in 44s

This commit is contained in:
张家训
2026-09-10 09:55:42 +08:00
parent deefce7856
commit 15cae333a3
4 changed files with 363 additions and 4 deletions
@@ -197,8 +197,12 @@ export default {
}
},
mounted () {
// 默认关闭所有的标签
this.handleTagsOption('all')
// 默认关闭所有的标签(仅在进入首页时重置)。
// 注意:直接输入地址/刷新进入子页面时不能重置,因为 handleTagsOption('all') 会触发
// 父组件的 handleCloseTag(res, 'all'),进而 turnToPage('home') 把页面弹回首页,导致深链接失效。
if (this.$route.name === this.$config.homeName) {
this.handleTagsOption('all')
}
setTimeout(() => {
this.getTagElementByName(this.$route)
}, 200)
+7
View File
@@ -154,6 +154,13 @@ export default {
this.addTag({
route: this.$store.state.app.homeRoute
})
// 直接输入地址/刷新进入时,localStorage 恢复的标签栏里没有当前页面,
// 这里把它补进标签栏,而不是直接跳回首页,否则深链接会被弹回首页
if (this.$route.name && this.$route.name !== this.$config.homeName) {
this.addTag({
route: this.$route
})
}
this.setBreadCrumb(this.$route)
// 如果当前打开页面不在标签栏中,跳到homeName页
if (!this.tagNavList.find(item => item.name === this.$route.name)) {
+35 -2
View File
@@ -24,6 +24,7 @@ const router = new Router({
routes,
mode: 'history'
})
const LOGIN_PAGE_NAME = 'login'
const turnTo = (to, access, next) => {
@@ -31,6 +32,36 @@ const turnTo = (to, access, next) => {
else next({replace: true, name: 'error_401'}) // 无权限,重定向到401页面
}
// 应用首次加载时的地址(此时 vue-router 尚未改写 URL),作为深链接兜底
const ENTRY_PATH = window.location.pathname + window.location.search + window.location.hash
/**
* 修正「直接输入地址 / 刷新页面」时深链接失效的问题。
*
* 动态菜单路由是在首次导航的守卫里才 addRoutes 的,而 vue-router 在进入守卫之前
* 就已经用通配符路由(path: '*' → redirect: '/home')把未注册的地址重定向掉了,
* 所以守卫里拿到的 to 已经是 /home,原始地址只保留在 to.redirectedFrom 中
* (vue-router 3.x 存的是 fullPath 字符串),表现为访问 /delta/gun-code 后停在首页「无反应」。
*
* 这里在动态路由注册完成后,用新的 matcher 重新匹配原始地址:命中真实路由则再跳一次。
* @returns {Boolean} 是否已重新发起跳转(true 时调用方不要再调用 next)
*/
const turnToRedirectedFrom = (to, next) => {
const redirectedFrom = to.redirectedFrom
// 兼容字符串(vue-router 3.x 实际类型)与对象两种形态,取不到时回退到入口地址
const fromPath = typeof redirectedFrom === 'string'
? redirectedFrom
: (redirectedFrom && redirectedFrom.path) || ENTRY_PATH
// 根路径本身就会重定向到首页;已经停在目标地址也无需再跳
if (!fromPath || fromPath === '/' || fromPath === to.fullPath) return false
const matched = router.match(fromPath)
if (matched.matched.length > 0 && matched.name !== '_no_match') {
next({ path: fromPath, replace: true })
return true
}
return false
}
router.beforeEach((to, from, next) => {
iView.LoadingBar.start()
const token = getToken()
@@ -67,7 +98,8 @@ router.beforeEach((to, from, next) => {
// 刷新界面菜单
store.commit('updateMenuList', list)
next()
// 动态路由已注册,若本次是深链接(被通配符重定向过)则回到原始地址
if (!turnToRedirectedFrom(to, next)) next()
}).catch(() => {
// 获取权限失败,直接放行(保留基础路由),避免卡死
next()
@@ -75,7 +107,8 @@ router.beforeEach((to, from, next) => {
} else {
let list = formatMenuList(JSON.parse(menuListStr))
store.commit('updateMenuList', list)
next()
// 动态路由已注册,若本次是深链接(被通配符重定向过)则回到原始地址
if (!turnToRedirectedFrom(to, next)) next()
}
}).catch(() => {
+315
View File
@@ -0,0 +1,315 @@
<template>
<div>
<!-- 顶部提示 start -->
<div class="entry-tip">
<Icon type="md-information-circle" size="16"/>
<span>录入后立即生效,公开复制页无需登录即可查看(仅「显示」状态的记录)。</span>
<Button type="text" size="small" icon="md-link" @click="copyAddress">复制公开页地址</Button>
</div>
<!-- 顶部提示 end -->
<Row :gutter="16">
<!-- 录入表单 start -->
<Col span="15">
<Card :bordered="false" dis-hover>
<p slot="title">改枪码录入</p>
<Form ref="formItem"
:model="formItem"
:rules="ruleCustom"
:label-width="90"
label-position="right"
@keydown.native.ctrl.enter="submit('formItem')">
<FormItem label="配装名称" prop="name">
<Input ref="nameInput" type="text" v-model="formItem.name" maxlength="100" show-word-limit
placeholder="必填,如:MP7 冲锋流"/>
</FormItem>
<Row :gutter="16">
<Col span="12">
<FormItem label="枪械/载具" prop="gunName">
<Input type="text" v-model="formItem.gunName" maxlength="100" placeholder="如:MP7"/>
</FormItem>
</Col>
<Col span="12">
<FormItem label="排序" prop="sort">
<InputNumber :min="0" v-model="formItem.sort" class="form-input" placeholder="数字越小越靠前"/>
</FormItem>
</Col>
</Row>
<FormItem label="分类标签" prop="category">
<Input type="text" v-model="formItem.category" maxlength="50" placeholder="如:主武器"/>
<div class="quick-category">
<Tag v-for="item in categoryOptions"
:key="item"
checkable
:checked="formItem.category === item"
@on-change="pickCategory(item)">{{ item }}</Tag>
</div>
</FormItem>
<FormItem label="改枪码" prop="code">
<Input type="textarea" :rows="8" v-model="formItem.code"
placeholder="必填,粘贴游戏内复制的改枪码"/>
</FormItem>
<FormItem label="备注" prop="remark">
<Input type="textarea" :rows="3" maxlength="200" show-word-limit v-model="formItem.remark"
placeholder="选填:配装思路 / 适用场景"/>
</FormItem>
<FormItem label="状态" prop="status">
<RadioGroup v-model="formItem.status">
<Radio :label="1">
<span>显示</span>
</Radio>
<Radio :label="0">
<span>隐藏</span>
</Radio>
</RadioGroup>
</FormItem>
<FormItem>
<Button type="primary" icon="md-checkmark" :loading="submitLoading" @click="submit('formItem')">
保存并继续录入
</Button>
<Button icon="md-refresh" style="margin-left: 8px" @click="resetForm">重置</Button>
<span class="shortcut-tip">快捷键:Ctrl + Enter</span>
</FormItem>
</Form>
</Card>
</Col>
<!-- 录入表单 end -->
<!-- 最近录入 start -->
<Col span="9">
<Card :bordered="false" dis-hover>
<p slot="title">最近录入</p>
<Button slot="extra" type="text" size="small" icon="md-refresh" @click="getRecentList">刷新</Button>
<div v-if="recentList.length === 0" class="empty-tip">暂无记录</div>
<div v-for="item in recentList" :key="item.id" class="recent-item">
<div class="recent-main">
<div class="recent-title">
<Tag :color="categoryColor(item.category)" size="small">{{ item.category || '未分类' }}</Tag>
<span class="recent-name">{{ item.name }}</span>
<span v-if="item.gunName" class="recent-gun">{{ item.gunName }}</span>
</div>
<div class="recent-code">{{ item.code }}</div>
<div class="recent-meta">{{ item.createBy || '-' }} · {{ formatDate(item.createDate) }}</div>
</div>
<Button type="text" size="small" icon="md-copy" @click="copyRow(item)">复制</Button>
</div>
</Card>
</Col>
<!-- 最近录入 end -->
</Row>
</div>
</template>
<script>
import { getDataList, add } from '@/api/delta/delta-gun-code'
import { copyText } from '@/common/js/clipboard'
import { formatDate } from '@/common/js/util'
export default {
name: 'delta-gun-code-entry',
data () {
return {
shareUrl: `${window.location.origin}/delta-gun-codes.html`, // 公开复制页面地址
categoryOptions: ['主武器', '副武器', '载具配置', '装备'], // 分类快捷选择
recentList: [], // 最近录入
submitLoading: false,
formItem: {
id: '',
name: '',
gunName: '',
category: '',
code: '',
remark: '',
sort: 0,
status: 1
},
ruleCustom: {
name: [{ required: true, message: '请输入配装名称', trigger: 'blur' }],
code: [{ required: true, message: '请输入改枪码', trigger: 'blur' }]
}
}
},
methods: {
formatDate (date) {
if (!date) return '-'
// 后端返回格式不固定(时间戳/ISO/字符串),无法解析时原样展示,避免出现 NaN
const time = new Date(date)
if (isNaN(time.getTime())) return date
return formatDate(date)
},
submit (formItem) {
this.$refs[formItem].validate((valid) => {
if (!valid) return
this.submitLoading = true
add(this.formItem).then(res => {
this.submitLoading = false
if (res.data.code !== 200) {
this.$Message.error(res.data.message || '录入失败')
return
}
this.$Message.success('录入成功!')
// 保留分类/排序/状态,方便连续录入同类配装
let keep = {
category: this.formItem.category,
sort: this.formItem.sort,
status: this.formItem.status
}
this.resetForm(keep)
this.getRecentList()
}).catch(() => {
this.submitLoading = false
})
})
},
resetForm (keep) {
let temp = keep || {}
this.formItem = {
id: '',
name: '',
gunName: '',
category: temp.category || '',
code: '',
remark: '',
sort: temp.sort === undefined ? 0 : temp.sort,
status: temp.status === undefined ? 1 : temp.status
}
this.$nextTick(() => {
// 仅清除校验状态,不能调用 resetFields(会把保留项一并重置)
let form = this.$refs.formItem
if (form && form.fields) {
form.fields.forEach(field => {
field.validateState = ''
field.validateMessage = ''
})
}
if (this.$refs.nameInput) {
this.$refs.nameInput.focus()
}
})
},
pickCategory (item) {
this.formItem.category = this.formItem.category === item ? '' : item
},
getRecentList () {
getDataList({ pageNum: 1, pageSize: 5 }).then(res => {
let page = res.data.data || {}
this.recentList = page.records || []
})
},
copyRow (item) {
copyText(item.code).then(ok => {
if (ok) {
this.$Message.success('改枪码已复制到剪贴板!')
} else {
this.$Message.error('复制失败,请手动选择复制')
}
})
},
copyAddress () {
copyText(this.shareUrl).then(ok => {
if (ok) {
this.$Message.success('公开页面地址已复制!')
} else {
this.$Message.error('复制失败,请手动选择复制')
}
})
},
categoryColor (category) {
let map = {
'主武器': 'red',
'副武器': 'blue',
'载具配置': 'green',
'装备': 'orange'
}
return map[category] || 'geekblue'
}
},
created () {
this.getRecentList()
}
}
</script>
<style lang="less" scoped>
.entry-tip {
display: flex;
align-items: center;
margin-bottom: 12px;
padding: 8px 12px;
background: #f0faff;
border: 1px solid #d6eaff;
border-radius: 4px;
color: #515a6e;
font-size: 12px;
i {
margin-right: 6px;
color: #2d8cf0;
}
}
.form-input {
width: 100%
}
.quick-category {
margin-top: 6px;
}
.shortcut-tip {
margin-left: 12px;
color: #c5c8ce;
font-size: 12px;
}
.empty-tip {
padding: 24px 0;
text-align: center;
color: #c5c8ce;
font-size: 12px;
}
.recent-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 0;
border-bottom: 1px solid #f0f0f0;
&:last-child {
border-bottom: none;
}
.recent-main {
flex: 1;
min-width: 0;
margin-right: 8px;
}
.recent-title {
display: flex;
align-items: center;
margin-bottom: 4px;
}
.recent-name {
font-weight: 600;
color: #17233d;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.recent-gun {
margin-left: 6px;
color: #808695;
font-size: 12px;
}
.recent-code {
color: #515a6e;
font-size: 12px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.recent-meta {
margin-top: 2px;
color: #c5c8ce;
font-size: 12px;
}
}
</style>