推送项目重构代码
This commit is contained in:
97
ruoyi-ui/src/layout/components/FeedbackEntry.vue
Normal file
97
ruoyi-ui/src/layout/components/FeedbackEntry.vue
Normal file
@@ -0,0 +1,97 @@
|
||||
<template>
|
||||
<span class="feedback-entry">
|
||||
<el-tooltip content="提交修改意见 / 反馈问题" effect="dark" placement="bottom">
|
||||
<el-button size="mini" type="text" icon="el-icon-edit-outline" class="entry-btn"
|
||||
@click="open = true">意见</el-button>
|
||||
</el-tooltip>
|
||||
|
||||
<el-dialog title="提交修改意见" :visible.sync="open" width="520px" append-to-body
|
||||
:close-on-click-modal="false">
|
||||
<el-form ref="formRef" :model="form" :rules="rules" size="mini" label-width="68px">
|
||||
<el-form-item label="标题" prop="title">
|
||||
<el-input v-model="form.title" placeholder="一句话概括问题或建议" maxlength="120" show-word-limit />
|
||||
</el-form-item>
|
||||
<el-form-item label="类型" prop="category">
|
||||
<el-radio-group v-model="form.category">
|
||||
<el-radio-button label="bug">Bug</el-radio-button>
|
||||
<el-radio-button label="feature">新功能</el-radio-button>
|
||||
<el-radio-button label="other">其他</el-radio-button>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="优先级" prop="priority">
|
||||
<el-radio-group v-model="form.priority">
|
||||
<el-radio :label="1">高</el-radio>
|
||||
<el-radio :label="2">中</el-radio>
|
||||
<el-radio :label="3">低</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="详细" prop="content">
|
||||
<el-input v-model="form.content" type="textarea" :autosize="{ minRows: 4, maxRows: 8 }"
|
||||
placeholder="描述问题复现步骤 / 期望的功能 / 建议改进点……" />
|
||||
</el-form-item>
|
||||
<el-form-item label="附件" prop="attachment">
|
||||
<file-upload v-model="form.attachment" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<span slot="footer">
|
||||
<el-button size="mini" @click="open = false">取消</el-button>
|
||||
<el-button size="mini" type="primary" :loading="submitting" @click="onSubmit">
|
||||
提交(会通过 IM 通知信息化部)
|
||||
</el-button>
|
||||
</span>
|
||||
</el-dialog>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { submitSuggestion } from '@/api/oa/suggestion'
|
||||
import FileUpload from '@/components/FileUpload'
|
||||
|
||||
export default {
|
||||
name: 'FeedbackEntry',
|
||||
components: { FileUpload },
|
||||
data () {
|
||||
return {
|
||||
open: false,
|
||||
submitting: false,
|
||||
form: this.emptyForm(),
|
||||
rules: {
|
||||
title: [{ required: true, message: '请输入标题', trigger: 'blur' }],
|
||||
content: [{ required: true, message: '请填写详细内容', trigger: 'blur' }],
|
||||
category: [{ required: true, message: '请选择类型', trigger: 'change' }]
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
emptyForm () {
|
||||
return {
|
||||
title: '', content: '', category: 'feature', priority: 2,
|
||||
attachment: '', pagePath: ''
|
||||
}
|
||||
},
|
||||
onSubmit () {
|
||||
this.$refs.formRef.validate(ok => {
|
||||
if (!ok) return
|
||||
this.submitting = true
|
||||
this.form.pagePath = this.$route && this.$route.fullPath
|
||||
submitSuggestion(this.form).then(() => {
|
||||
this.$modal.msgSuccess('已提交,感谢反馈!')
|
||||
this.open = false
|
||||
this.form = this.emptyForm()
|
||||
}).finally(() => { this.submitting = false })
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.feedback-entry { display: inline-block; }
|
||||
.entry-btn {
|
||||
padding: 4px 8px !important;
|
||||
font-size: 12px !important;
|
||||
color: #e6a23c;
|
||||
i { margin-right: 2px; }
|
||||
&:hover { color: #f56c6c; }
|
||||
}
|
||||
</style>
|
||||
@@ -9,17 +9,7 @@
|
||||
<div class="right-menu">
|
||||
<template v-if="device !== 'mobile'">
|
||||
<search id="header-search" class="right-menu-item" />
|
||||
|
||||
<!-- <div style="position: absolute; top: 0; right: 300px; font-weight: 200">
|
||||
<el-button class="el-icon-s-comment" @click="chat = true" style=""
|
||||
>打开聊天</el-button
|
||||
>
|
||||
<chat-component
|
||||
:drawerVisible="chat"
|
||||
ref="chatComponent"
|
||||
@close="hiddenChat"
|
||||
/>
|
||||
</div> -->
|
||||
<feedback-entry class="right-menu-item" />
|
||||
<screenfull id="screenfull" class="right-menu-item hover-effect" />
|
||||
<el-tooltip content="用户" effect="dark" placement="bottom">
|
||||
<div class="right-menu-item hover-effect">
|
||||
@@ -62,6 +52,7 @@ import Screenfull from "@/components/Screenfull";
|
||||
import SizeSelect from "@/components/SizeSelect";
|
||||
import TopNav from "@/components/TopNav";
|
||||
import AIChat from "@/layout/components/AIChat/index.vue";
|
||||
import FeedbackEntry from "@/layout/components/FeedbackEntry.vue";
|
||||
import { parseTime } from "@/utils/ruoyi";
|
||||
import { mapGetters } from "vuex";
|
||||
// import {
|
||||
@@ -81,6 +72,7 @@ export default {
|
||||
RuoYiGit,
|
||||
RuoYiDoc,
|
||||
AIChat,
|
||||
FeedbackEntry,
|
||||
},
|
||||
computed: {
|
||||
// chatComponent() {
|
||||
|
||||
207
ruoyi-ui/src/layout/components/TutorialGuide.vue
Normal file
207
ruoyi-ui/src/layout/components/TutorialGuide.vue
Normal file
@@ -0,0 +1,207 @@
|
||||
<template>
|
||||
<div v-if="active" class="tutorial-overlay" @click.self="next">
|
||||
<div class="tutorial-card" :style="cardStyle">
|
||||
<div class="tut-title">
|
||||
<span class="step-num">{{ stepIndex + 1 }} / {{ steps.length }}</span>
|
||||
{{ currentStep.title }}
|
||||
</div>
|
||||
<div class="tut-body" v-html="currentStep.html" />
|
||||
<div class="tut-footer">
|
||||
<el-button size="mini" type="text" @click="finish">跳过</el-button>
|
||||
<div style="flex:1" />
|
||||
<el-button v-if="stepIndex > 0" size="mini" @click="prev">上一步</el-button>
|
||||
<el-button size="mini" type="primary" @click="next">
|
||||
{{ stepIndex === steps.length - 1 ? '完成' : '下一步' }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 高亮目标元素的"挖空" -->
|
||||
<div v-if="holeStyle" class="tutorial-hole" :style="holeStyle" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
const VERSION = 'oa_tutorial_v2' // 升级版本号即可让所有人重看一次
|
||||
|
||||
const STEPS = [
|
||||
{
|
||||
title: '👋 欢迎使用',
|
||||
html: '<p>这是一个简短的引导,约 6 步,带您熟悉常用入口。</p><p>任意时候点空白处或按"跳过"退出引导,下次不会再出现。</p>'
|
||||
},
|
||||
{
|
||||
target: '.feedback-entry',
|
||||
title: '提交修改意见',
|
||||
html: '遇到问题或想提建议?点这里提交反馈,会自动 IM 通知信息化部门。'
|
||||
},
|
||||
{
|
||||
target: '#header-search',
|
||||
title: '快速搜索',
|
||||
html: '不知道功能在哪?这里输入菜单名就能跳转。'
|
||||
},
|
||||
{
|
||||
target: '.sidebar-container',
|
||||
title: '左侧菜单',
|
||||
html: '所有功能按模块分组。鼠标移上去看二级菜单。'
|
||||
},
|
||||
{
|
||||
target: '.workbench-edit-fab, .workbench, .home',
|
||||
title: '个人工作台',
|
||||
html: '首页是您的个人工作台,可以添加/拖拽组件(待办、聊天、进度等),每个人都不一样。'
|
||||
},
|
||||
{
|
||||
title: '🎉 开始使用',
|
||||
html: '随时可在浏览器地址栏添加 <code>?tutorial=1</code> 重新打开本引导。<br>祝您工作顺利~'
|
||||
}
|
||||
]
|
||||
|
||||
export default {
|
||||
name: 'TutorialGuide',
|
||||
data () {
|
||||
return {
|
||||
active: false,
|
||||
stepIndex: 0,
|
||||
steps: STEPS,
|
||||
targetRect: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
currentStep () { return this.steps[this.stepIndex] || {} },
|
||||
cardStyle () {
|
||||
if (!this.targetRect) {
|
||||
// 居中
|
||||
return { top: '40vh', left: '50%', transform: 'translateX(-50%)' }
|
||||
}
|
||||
const r = this.targetRect
|
||||
// 放在目标下方,如果超出则放上方
|
||||
const cardH = 200
|
||||
const margin = 12
|
||||
let top = r.bottom + margin
|
||||
if (top + cardH > window.innerHeight) top = Math.max(20, r.top - cardH - margin)
|
||||
let left = r.left
|
||||
if (left + 380 > window.innerWidth) left = window.innerWidth - 380 - 12
|
||||
return { top: top + 'px', left: Math.max(12, left) + 'px' }
|
||||
},
|
||||
holeStyle () {
|
||||
if (!this.targetRect) return null
|
||||
const r = this.targetRect
|
||||
const pad = 4
|
||||
return {
|
||||
top: (r.top - pad) + 'px',
|
||||
left: (r.left - pad) + 'px',
|
||||
width: (r.width + pad * 2) + 'px',
|
||||
height: (r.height + pad * 2) + 'px'
|
||||
}
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.$nextTick(this.maybeStart)
|
||||
},
|
||||
mounted () {
|
||||
window.addEventListener('resize', this.refreshTarget)
|
||||
},
|
||||
beforeDestroy () {
|
||||
window.removeEventListener('resize', this.refreshTarget)
|
||||
},
|
||||
watch: {
|
||||
'$route' () {
|
||||
if (!this.active) this.maybeStart()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
maybeStart () {
|
||||
const force = (this.$route && this.$route.query && this.$route.query.tutorial) === '1'
|
||||
const done = !force && localStorage.getItem(VERSION) === '1'
|
||||
if (done) return
|
||||
// 等 layout 渲染完
|
||||
setTimeout(() => {
|
||||
this.active = true
|
||||
this.stepIndex = 0
|
||||
this.refreshTarget()
|
||||
}, 800)
|
||||
},
|
||||
refreshTarget () {
|
||||
const sel = this.currentStep.target
|
||||
if (!sel) { this.targetRect = null; return }
|
||||
const el = document.querySelector(sel)
|
||||
if (!el) { this.targetRect = null; return }
|
||||
this.targetRect = el.getBoundingClientRect()
|
||||
},
|
||||
prev () {
|
||||
if (this.stepIndex > 0) {
|
||||
this.stepIndex--
|
||||
this.$nextTick(this.refreshTarget)
|
||||
}
|
||||
},
|
||||
next () {
|
||||
if (this.stepIndex === this.steps.length - 1) {
|
||||
this.finish()
|
||||
return
|
||||
}
|
||||
this.stepIndex++
|
||||
this.$nextTick(this.refreshTarget)
|
||||
},
|
||||
finish () {
|
||||
this.active = false
|
||||
localStorage.setItem(VERSION, '1')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tutorial-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.55);
|
||||
z-index: 9999;
|
||||
}
|
||||
.tutorial-hole {
|
||||
position: absolute;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.55);
|
||||
pointer-events: none;
|
||||
border: 2px solid #409eff;
|
||||
transition: all .25s;
|
||||
}
|
||||
.tutorial-card {
|
||||
position: absolute;
|
||||
width: 380px;
|
||||
background: #fff;
|
||||
border-radius: 6px;
|
||||
padding: 14px 16px;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
|
||||
z-index: 10000;
|
||||
}
|
||||
.tut-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
color: #303133;
|
||||
.step-num {
|
||||
background: #ecf5ff;
|
||||
color: #409eff;
|
||||
border-radius: 8px;
|
||||
font-size: 11px;
|
||||
padding: 2px 6px;
|
||||
margin-right: 6px;
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
.tut-body {
|
||||
font-size: 12px;
|
||||
color: #606266;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 10px;
|
||||
::v-deep code {
|
||||
background: #f0f0f0;
|
||||
padding: 1px 4px;
|
||||
border-radius: 3px;
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
.tut-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
</style>
|
||||
@@ -12,12 +12,14 @@
|
||||
<settings/>
|
||||
</right-panel>
|
||||
</div>
|
||||
<tutorial-guide />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import RightPanel from '@/components/RightPanel'
|
||||
import { AppMain, Navbar, Settings, Sidebar, TagsView } from './components'
|
||||
import TutorialGuide from './components/TutorialGuide.vue'
|
||||
import ResizeMixin from './mixin/ResizeHandler'
|
||||
import { mapState } from 'vuex'
|
||||
import variables from '@/assets/styles/variables.scss'
|
||||
@@ -28,6 +30,7 @@ export default {
|
||||
AppMain,
|
||||
Navbar,
|
||||
RightPanel,
|
||||
TutorialGuide,
|
||||
Settings,
|
||||
Sidebar,
|
||||
TagsView,
|
||||
|
||||
Reference in New Issue
Block a user