推送项目重构代码

This commit is contained in:
2026-05-30 15:32:57 +08:00
parent 3dafaceef2
commit a28ea44cab
53 changed files with 3525 additions and 731 deletions

View 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>

View File

@@ -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() {

View 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>

View File

@@ -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,