Loading... # 前言 麻烦给个三连吧~~~ 课程·任务书和源码都在: 通过网盘分享的文件:服装生产销售管理系统.zip 链接: https://pan.baidu.com/s/12Bz7XWsq5EJdztW_F4NaNQ?pwd=yf5g 提取码: yf5g 文件中的 综合课程设计.pdf 是java课程和软件工程的综合设计题目,我的选题是`服装生产销售管理系统`,当时不会写可,最后一晚上搞定的,从网站找了一个类似的项目,源码文档数据库都在原始版本的文件夹里面,最后我修改的那本的在oisec修改版文件及中,只有最后一个导出报表的还没有实现,由于课程结束就不再想去动他了,希望有大佬能够帮忙实现吧,传递为爱发电。鄙人由于才疏学浅,课设时间紧迫,亟需展示效果,所以改的可能不是很好,期待指导。 # 修改的部分 ## 登录界面划分四个权限用户`login.vue` ```vue <template> <div> <div class="container loginIn" style="backgroundImage: url(http://imag.keyblue.cn/image/fuzhuang.png)"> <div :class="2 == 1 ? 'left' : 2 == 2 ? 'left center' : 'left right'" style="backgroundColor: rgba(255, 255, 255, 0.5)"> <el-form class="login-form" label-position="left" :label-width="2 == 3 ? '56px' : '0px'"> <div class="title-container"><h3 class="title" style="color: rgba(13, 102, 171, 1)">服装生产销售管理系统</h3></div> <el-form-item :label="2 == 3 ? '用户名' : ''" :class="'style'+2"> <span v-if="2 != 3" class="svg-container" style="color:rgba(13, 102, 171, 1);line-height:44px"><svg-icon icon-class="user" /></span> <el-input placeholder="请输入用户名" name="username" type="text" v-model="rulesForm.username" /> </el-form-item> <el-form-item :label="2 == 3 ? '密码' : ''" :class="'style'+2"> <span v-if="2 != 3" class="svg-container" style="color:rgba(13, 102, 171, 1);line-height:44px"><svg-icon icon-class="password" /></span> <el-input placeholder="请输入密码" name="password" type="password" v-model="rulesForm.password" /> </el-form-item> <el-form-item v-if="0 == '1'" class="code" :label="2 == 3 ? '验证码' : ''" :class="'style'+2"> <span v-if="2 != 3" class="svg-container" style="color:rgba(13, 102, 171, 1);line-height:44px"><svg-icon icon-class="code" /></span> <el-input placeholder="请输入验证码" name="code" type="text" v-model="rulesForm.code" /> <div class="getCodeBt" @click="getRandCode(4)" style="height:44px;line-height:44px"> <span v-for="(item, index) in codes" :key="index" :style="{color:item.color,transform:item.rotate,fontSize:item.size}"><ruby>item.num }}</span> </div> </el-form-item> <el-form-item label="角色" prop="loginInRole" class="role"> <el-radio v-for="item in menus" v-if="item.hasBackLogin=='是'" v-bind<rp> (</rp><rt>key="item.roleName" v-model="rulesForm.role" :label="item.roleName" >{{item.roleName</rt><rp>) </rp></ruby></el-radio> </el-form-item> <el-button type="primary" @click="login()" class="loginInBt" style="padding:0;font-size:16px;border-radius:4px;height:44px;line-height:44px;width:100%;backgroundColor:rgba(13, 102, 171, 1); borderColor:rgba(13, 102, 171, 1); color:rgba(255, 255, 255, 1)"><ruby>'1' == '1' ? '登录'<rp> (</rp><rt>'login'</rt><rp>) </rp></ruby></el-button> <el-form-item class="setting"> <!-- <div style="color:rgba(13, 102, 171, 1)" class="reset">修改密码</div> --> </el-form-item> </el-form> </div> </div> </div> </template> <script> import menu from "@/utils/menu"; export default { data() { return { rulesForm: { username: "", password: "", role: "", code: '', }, menus: [], tableName: "", codes: [{ num: 1, color: '#000', rotate: '10deg', size: '16px' },{ num: 2, color: '#000', rotate: '10deg', size: '16px' },{ num: 3, color: '#000', rotate: '10deg', size: '16px' },{ num: 4, color: '#000', rotate: '10deg', size: '16px' }], }; }, mounted() { let menus = menu.list(); this.menus = menus; }, created() { this.setInputColor() this.getRandCode() }, methods: { setInputColor(){ this.$nextTick(()=>{ document.querySelectorAll('.loginIn .el-input__inner').forEach(el=>{ el.style.backgroundColor = "rgba(247, 247, 247, 1)" el.style.color = "rgba(51, 51, 51, 1)" el.style.height = "44px" el.style.lineHeight = "44px" el.style.borderRadius = "4px" }) document.querySelectorAll('.loginIn .style3 .el-form-item__label').forEach(el=>{ el.style.height = "44px" el.style.lineHeight = "44px" }) document.querySelectorAll('.loginIn .el-form-item__label').forEach(el=>{ el.style.color = "rgba(13, 102, 171, 1)" }) setTimeout(()=>{ document.querySelectorAll('.loginIn .role .el-radio__label').forEach(el=>{ el.style.color = "rgba(13, 102, 171, 1)" }) },350) }) }, register(tableName){ this.$storage.set("loginTable", tableName); this.$router.push({path:'/register'}) }, // 登陆 login() { let code = '' for(let i in this.codes) { code += this.codes[i].num } if ('0' == '1' && !this.rulesForm.code) { this.$message.error("请输入验证码"); return; } if ('0' == '1' && this.rulesForm.code.toLowerCase() != code.toLowerCase()) { this.$message.error("验证码输入有误"); this.getRandCode() return; } if (!this.rulesForm.username) { this.$message.error("请输入用户名"); return; } if (!this.rulesForm.password) { this.$message.error("请输入密码"); return; } if (!this.rulesForm.role) { this.$message.error("请选择角色"); return; } // if (this.rulesForm.role) { // this.$message.error("该账户已分配角色: " + this.rulesForm.role + ",无法登录其他角色。"); // return; // } let menus = this.menus; for (let i = 0; i < menus.length; i++) { if (menus[i].roleName == this.rulesForm.role) { this.tableName = menus[i].tableName; } } this.$http({ url: `${this.tableName}/login?username=${this.rulesForm.username}&password=${this.rulesForm.password}&role=${this.rulesForm.role}`, method: "post" }).then(({ data }) => { if (data && data.code === 0) { this.$storage.set("Token", data.token); this.$storage.set("role", this.rulesForm.role); this.$storage.set("sessionTable", this.tableName); this.$storage.set("adminName", this.rulesForm.username); this.$router.replace({ path: "/index/" }); } else { this.$message.error(data.msg); } }); }, getRandCode(len = 4){ this.randomString(len) }, randomString(len = 4) { let chars = [ "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9" ] let colors = ["0", "1", "2","3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d", "e", "f"] let sizes = ['14', '15', '16', '17', '18'] let output = []; for (let i = 0; i < len; i++) { // 随机验证码 let key = Math.floor(Math.random()*chars.length) this.codes[i].num = chars[key] // 随机验证码颜色 let code = '#' for (let j = 0; j < 6; j++) { let key = Math.floor(Math.random()*colors.length) code += colors[key] } this.codes[i].color = code // 随机验证码方向 let rotate = Math.floor(Math.random()*60) let plus = Math.floor(Math.random()*2) if(plus == 1) rotate = '-'+rotate this.codes[i].rotate = 'rotate('+rotate+'deg)' // 随机验证码字体大小 let size = Math.floor(Math.random()*sizes.length) this.codes[i].size = sizes[size]+'px' } }, } }; </script> <style lang="scss" scoped> .loginIn { min-height: 100vh; position: relative; background-repeat: no-repeat; background-position: center center; background-size: cover; .left { position: absolute; left: 0; top: 0; width: 360px; height: 100%; .login-form { background-color: transparent; width: 100%; right: inherit; padding: 0 12px; box-sizing: border-box; display: flex; justify-content: center; flex-direction: column; } .title-container { text-align: center; font-size: 24px; .title { margin: 20px 0; } } .el-form-item { position: relative; .svg-container { padding: 6px 5px 6px 15px; color: #889aa4; vertical-align: middle; display: inline-block; position: absolute; left: 0; top: 0; z-index: 1; padding: 0; line-height: 40px; width: 30px; text-align: center; } .el-input { display: inline-block; height: 40px; width: 100%; & /deep/ input { background: transparent; border: 0px; -webkit-appearance: none; padding: 0 15px 0 30px; color: #fff; height: 40px; } } } } .center { position: absolute; left: 50%; top: 50%; width: 360px; transform: translate3d(-50%,-50%,0); height: 446px; border-radius: 8px; } .right { position: absolute; left: inherit; right: 0; top: 0; width: 360px; height: 100%; } .code { .el-form-item__content { position: relative; .getCodeBt { position: absolute; right: 0; top: 0; line-height: 40px; width: 100px; background-color: rgba(51,51,51,0.4); color: #fff; text-align: center; border-radius: 0 4px 4px 0; height: 40px; overflow: hidden; span { padding: 0 5px; display: inline-block; font-size: 16px; font-weight: 600; } } .el-input { & /deep/ input { padding: 0 130px 0 30px; } } } } .setting { & /deep/ .el-form-item__content { padding: 0 15px; box-sizing: border-box; line-height: 32px; height: 32px; font-size: 14px; color: #999; margin: 0 !important; .register { float: left; width: 50%; } .reset { float: right; width: 50%; text-align: right; } } } .style2 { padding-left: 30px; .svg-container { left: -30px !important; } .el-input { & /deep/ input { padding: 0 15px !important; } } } .code.style2, .code.style3 { .el-input { & /deep/ input { padding: 0 115px 0 15px; } } } .style3 { & /deep/ .el-form-item__label { padding-right: 6px; } .el-input { & /deep/ input { padding: 0 15px !important; } } } .role { .el-radio { margin-right: 20px; padding: 10px; border-radius: 5px; transition: background-color 0.3s, transform 0.3s; &:hover { background-color: rgba(13, 102, 171, 0.1); transform: scale(1.05); } &.is-checked { background-color: rgba(13, 102, 171, 0.3); } } } } </style> ``` ## 登录的后端接口需要添加用户的权限角色对比认证UserController.java ```java /** * 登录 */ @IgnoreAuth @PostMapping(value = "/login") public R login(String username, String password, String role, String captcha, HttpServletRequest request) { UserEntity user = userService.selectOne(new EntityWrapper<UserEntity>().eq("username", username)); if(user==null || !user.getPassword().equals(password)) { return R.error("账号或密码不正确"); } // 检查用户提交的角色是否与数据库中的角色一致 if (!user.getRole().equals(role)) { return R.error("角色不匹配,请选择正确的角色登录"); } String token = tokenService.generateToken(user.getId(),username, "users", user.getRole()); return R.ok().put("token", token); } ``` ## 每个用户登录后会有不同的功能列表menu.js ```js const menu = { list() { return [ {//系统管理员 "backMenu": [{ "child": [{ "buttons": ["新增", "查看", "修改", "删除"], "menu": "用户", "menuJump": "列表", "tableName": "yonghu" }], "menu": "用户管理" }, { "child": [{ "buttons": ["查看"], "menu": "生产列表", "menuJump": "列表", "tableName": "yangban" }], "menu": "生产模块" },{ "child": [{ "buttons": ["新增", "查看", "修改", "删除"], "menu": "订单", "menuJump": "列表", "tableName": "dingdan" }], "menu": "订单管理" }, { "child": [{ "buttons": ["新增", "查看", "修改", "删除", "入库", "出库"], "menu": "服装仓库", "menuJump": "列表", "tableName": "yuanliaocangku" }], "menu": "服装仓库管理" }, { "child": [{ "buttons": ["新增", "查看", "修改", "删除"], "menu": "采购管理", "menuJump": "列表", "tableName": "yuanliaoruku" }], "menu": "采购入库管理" }, { "child": [{ "buttons": ["新增", "查看", "修改", "删除"], "menu": "销售管理", "menuJump": "列表", "tableName": "yuanliaochuku" }], "menu": "销售出库管理" }, { "child": [{ "buttons": ["查看", "修改", "删除", "新增", "出库"], "menu": "成衣仓库", "menuJump": "列表", "tableName": "chengyicangku" }], "menu": "成衣仓库管理" }, { "child": [{ "buttons": ["新增", "查看", "修改", "删除"], "menu": "成衣出库", "menuJump": "列表", "tableName": "chengyichuku" }], "menu": "成衣出库管理" }, { "child": [{ "buttons": ["新增", "查看", "修改", "删除"], "menu": "系统公告", "tableName": "news" }, { "buttons": ["查看", "修改"], "menu": "轮播图管理", "tableName": "config" }], "menu": "系统管理" },{ "child": [{ "buttons": ["新增", "查看", "修改", "删除"], "menu": "导出报表", "menuJump": "列表", "tableName": "renshianpai" }], "menu": "导出统计报表" }], "frontMenu": [{ "child": [{ "buttons": ["查看"], "menu": "生产列表", "menuJump": "列表", "tableName": "yangban" }], "menu": "生产模块" }], "hasBackLogin": "是", "hasBackRegister": "是", "hasFrontLogin": "否", "hasFrontRegister": "否", "roleName": "系统管理员", "tableName": "users" }, {//服装生产商 "backMenu": [{ "child": [{ "buttons": ["查看"], "menu": "生产列表", "menuJump": "列表", "tableName": "yangban" }], "menu": "生产模块" }], "frontMenu": [{ "child": [{ "buttons": ["查看"], "menu": "生产列表", "menuJump": "列表", "tableName": "yangban" }], "menu": "生产模块" }], "hasBackLogin": "是", "hasBackRegister": "是", "hasFrontLogin": "否", "hasFrontRegister": "否", "roleName": "服装生产商", "tableName": "users" }, { //服装销售商 "backMenu": [{ "child": [{ "buttons": ["新增", "查看", "修改", "删除"], "menu": "采购管理", "menuJump": "列表", "tableName": "yuanliaoruku" }], "menu": "采购入库管理" }, { "child": [{ "buttons": ["新增", "查看", "修改", "删除"], "menu": "销售管理", "menuJump": "列表", "tableName": "yuanliaochuku" }], "menu": "销售出库管理" }], "hasBackLogin": "是", "hasBackRegister": "是", "hasFrontLogin": "否", "hasFrontRegister": "否", "roleName": "服装销售商", "tableName": "users" }, { //仓库管理员 "backMenu": [{ "child": [{ "buttons": ["查看", "修改", "删除", "新增", "出库"], "menu": "成衣仓库", "menuJump": "列表", "tableName": "chengyicangku" }], "menu": "成衣仓库管理" }, { "child": [{ "buttons": ["新增", "查看", "修改", "删除"], "menu": "成衣出库", "menuJump": "列表", "tableName": "chengyichuku" }], "menu": "成衣出库管理" }], "hasBackLogin": "是", "hasBackRegister": "是", "hasFrontLogin": "否", "hasFrontRegister": "否", "roleName": "仓库管理员", "tableName": "users" }, { "backMenu": [ { "child": [{ "buttons": ["查看"], "menu": "生产列表", "menuJump": "列表", "tableName": "yangban" }], "menu": "生产模块" }], "hasBackLogin": "否", "hasBackRegister": "否", "hasFrontLogin": "是", "hasFrontRegister": "是", "roleName": "用户", "tableName": "yonghu" } ] } }; export default menu; ``` ## 生成识别二维码功能home.vue ```vue <template> <div class="home-container"> <!-- 生成二维码区域 --> <div class="qr-section"> <div class="qr-box"> <h2>生成生产批次二维码</h2> <div class="qr-content"> <div ref="qrcode" class="qr-code"></div> <div class="qr-info"> <p>批次号:<ruby>batchNumber }}</p> <p>生成时间:{{ currentTime }}</p> </div> <el-button type="primary" class="action-button"@click="generateQRCode">生成新批次二维码</el-button> <el-button type="success" class="action-button"@click="saveQRCode">保存到样板</el-button> <el-button type="success" class="action-button"@click="saveQRCode">保存到样板</el-button> <el-button type="success" class="action-button"@click="downloadQRCode">下载二维码</el-button> </div> </div> </div> <!-- 识别二维码区域 --> <div class="qr-section"> <div class="qr-box"> <h2>识别二维码</h2> <div class="scanner-content"> <video ref="video" class="scanner"></video> <canvas ref="canvas" style="display<rp> (</rp><rt>none;"></canvas> <div class="scan-result" v-if="scanResult"> <p>识别结果:{{ scanResult</rt><rp>) </rp></ruby></p> </div> <el-button type="primary" class="action-button" @click="startScanner">开始扫描</el-button> <el-button type="success" class="action-button" @click="openPurchaseForm">录入采购信息</el-button> <el-button type="success" class="action-button" @click="openPurchaseForm2">录入销售信息</el-button> <el-button type="success" class="action-button" @click="openPurchaseForm3">录入仓库信息</el-button> <el-button type="danger" class="action-button" @click="stopScanner">停止扫描</el-button> </div> </div> </div> </div> </template> <script> import router from '@/router/router-static' import QRCode from 'qrcode' import jsQR from 'jsqr' export default { mounted(){ this.init(); }, methods:{ init(){ if(this.$storage.get('Token')){ this.$http({ url: `${this.$storage.get('sessionTable')}/session`, method: "get" }).then(({ data }) => { if (data && data.code != 0) { router.push({ name: 'login' }) } }); }else{ router.push({ name: 'login' }) }s } }, name: 'Home', data() { return { batchNumber: '', currentTime: '', scanResult: '', scanning: false, stream: null, ruleForm: { yangbanmingcheng: '', mianliao: '', fuliao: '', yangbanchima: '', buweichicun: '', kuanshixinxi: '', zhuyidian: '', zhizuojindu: '', yangbantupian: '', }, yuanliaoru: { yuanliaobianhao: '', yuanliaomingcheng: '', shuliang: '', guige: '', pinpai: '', jiage: '', rukushijian: '', }, yuanliaochu: { yuanliaobianhao: '', yuanliaomingcheng: '', shuliang: '', guige: '', pinpai: '', jiage: '', chukushijian: '', }, cangku: { chengpinbianhao: '', chengpinmingcheng: '', shifouhege: '', jianyanren: '', jianyanshijian: '', shuliang: '', rukushijian: '', }, } }, methods: { base64ToBlob(base64Data, contentType) { const byteCharacters = atob(base64Data) const byteArrays = [] for (let offset = 0; offset < byteCharacters.length; offset += 512) { const slice = byteCharacters.slice(offset, offset + 512) const byteNumbers = new Array(slice.length) for (let i = 0; i < slice.length; i++) { byteNumbers[i] = slice.charCodeAt(i) } const byteArray = new Uint8Array(byteNumbers) byteArrays.push(byteArray) } return new Blob(byteArrays, { type: contentType }) }, // 生成二维码 async generateQRCode() { this.currentTime = new Date().toLocaleString() this.batchNumber = 'BATCH-' + Date.now() const qrData = { batchNumber: this.batchNumber, timestamp: Date.now(), createTime: this.currentTime } try { const qrCodeUrl = await QRCode.toDataURL(JSON.stringify(qrData)) const qrDiv = this.$refs.qrcode qrDiv.innerHTML = `<img src="${qrCodeUrl}" alt="QR Code"style="">` } catch (err) { console.error(err) } }, async downloadQRCode() { const qrImage = this.$refs.qrcode.querySelector('img').src // 将 base64 转换为文件 const base64Data = qrImage.split(',')[1] //console.log(base64Data) const blob = this.base64ToBlob(base64Data, 'image/png') //console.log(blob) const file = new File([blob], `qr-${Date.now()}.png`, { type: 'image/png' }) // 创建 FormData //console.log(file) const formData = new FormData() //console.log("准备上传的formData:", formData) formData.append('file', file) console.log("准备上传的文件:", file) // 先上传文件获取URL const uploadResponse = await this.$http({ url: 'file/upload', // 你的文件上传接口 method: 'post', data: formData, headers: { 'Content-Type': 'multipart/form-data' } }) //console.log("===============") //console.log(uploadResponse) // 这里调用你的 API 保存数据 const fileName = uploadResponse.data.file; // 获取文件名 const fileUrl = `http://localhost:8081/springbootww862/upload/${fileName}`; // 构造完整路径 //console.log("上传成功,文件访问路径:", fileUrl); if (fileUrl) { const link = document.createElement('a'); link.href = fileUrl; // 设置下载链接 link.download = 'qr-code.png'; // 设置下载文件名 document.body.appendChild(link); // 将链接添加到文档 link.click(); // 模拟点击下载 document.body.removeChild(link); // 下载后移除链接 } else { this.$message.error('没有可下载的二维码'); } }, // 保存二维码到样板 async saveQRCode() { const qrImage = this.$refs.qrcode.querySelector('img').src // 将 base64 转换为文件 const base64Data = qrImage.split(',')[1] //console.log(base64Data) const blob = this.base64ToBlob(base64Data, 'image/png') //console.log(blob) const file = new File([blob], `qr-${Date.now()}.png`, { type: 'image/png' }) // 创建 FormData //console.log(file) const formData = new FormData() //console.log("准备上传的formData:", formData) formData.append('file', file) //console.log("准备上传的文件:", file) // 先上传文件获取URL const uploadResponse = await this.$http({ url: 'file/upload', // 你的文件上传接口 method: 'post', data: formData, headers: { 'Content-Type': 'multipart/form-data' } }) //console.log("===============") //console.log(uploadResponse) // 这里调用你的 API 保存数据 const fileName = uploadResponse.data.file; // 获取文件名 const fileUrl = `http://localhost:8081/springbootww862/upload/${fileName}`; // 构造完整路径 //console.log("上传成功,文件访问路径:", fileUrl); const data = { fuliao: this.batchNumber, kuanshixinxi: fileUrl, createTime: this.currentTime } this.ruleForm.fuliao = this.batchNumber this.ruleForm.kuanshixinxi = fileUrl this.ruleForm.createTime = this.currentTime //console.log("===============") //console.log(this.ruleForm) // 调用保存 API try { // 发送请求 const { data } = await this.$http({ url: 'yangban/save', method: 'post', data: this.ruleForm }) if (data && data.code === 0) { this.$message({ message: '保存成功', type: 'success', duration: 1500, onClose: () => { // 可以添加保存成功后的操作,比如跳转到列表页 this.$router.push('/yangban') } }) } else { this.$message.error(data.msg) } }catch (err) { this.$message.error('保存失败') } }, // 开始扫描 async startScanner() { try { this.stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: 'environment' } }) this.$refs.video.srcObject = this.stream this.$refs.video.play() this.scanning = true this.scanQRCode() } catch (err) { console.error(err) } }, async openPurchaseForm(){ try { // 发送请求 const { data } = await this.$http({ url: '/yuanliaoruku/save', method: 'post', data: this.yuanliaoru }) if (data && data.code === 0) { this.$message({ message: '录入信息成功', type: 'success', duration: 1500, onClose: () => { this.$router.push('/yuanliaoruku') } }) } else { this.$message.error(data.msg) } }catch (err) { this.$message.error('录入信息失败') } }, async openPurchaseForm2(){ try { // 发送请求 const { data } = await this.$http({ url: '/yuanliaochuku/save', method: 'post', data: this.yuanliaochu }) if (data && data.code === 0) { this.$message({ message: '录入信息成功', type: 'success', duration: 1500, onClose: () => { this.$router.push('/yuanliaochuku') } }) } else { this.$message.error(data.msg) } }catch (err) { this.$message.error('录入信息失败') } }, async openPurchaseForm3(){ try { // 发送请求 const { data } = await this.$http({ url: '/chengyicangku/save', method: 'post', data: this.cangku }) if (data && data.code === 0) { this.$message({ message: '录入信息成功', type: 'success', duration: 1500, onClose: () => { this.$router.push('/chengyicangku') } }) } else { this.$message.error(data.msg) } }catch (err) { this.$message.error('录入信息失败') } }, // 停止扫描 async stopScanner() { if (this.stream) { this.stream.getTracks().forEach(track => track.stop()) this.scanning = false } }, // 扫描二维码 async scanQRCode() { if (!this.scanning) return const video = this.$refs.video const canvas = this.$refs.canvas const context = canvas.getContext('2d') if (video.readyState === video.HAVE_ENOUGH_DATA) { canvas.width = video.videoWidth canvas.height = video.videoHeight context.drawImage(video, 0, 0, canvas.width, canvas.height) const imageData = context.getImageData(0, 0, canvas.width, canvas.height) const code = jsQR(imageData.data, imageData.width, imageData.height) if (code) { this.scanResult = code.data const resultData = JSON.parse(this.scanResult); // 解析 scanResult this.yuanliaoru.yuanliaomingcheng = resultData.batchNumber; // 将 batchNumber 赋值给 yuanliaomingcheng const now = new Date(); this.yuanliaoru.rukushijian = now.toISOString().slice(0, 19).replace('T', ' '); // 格式化为 'YYYY-MM-DD HH:MM:SS' //录入采购信息 this.yuanliaochu.yuanliaomingcheng = resultData.batchNumber; this.yuanliaochu.chukushijian = now.toISOString().slice(0, 19).replace('T', ' '); // 格式化为 'YYYY-MM-DD HH:MM:SS' //录入销售信息 this.cangku.chengpinbianhao = resultData.batchNumber; // 将 batchNumber 赋值给 yuanliaomingcheng this.cangku.rukushijian = now.toISOString().slice(0, 19).replace('T', ' '); // 格式化为 'YYYY-MM-DD HH:MM:SS' console.log(this.cangku) this.stopScanner() } } if (this.scanning) { requestAnimationFrame(() => this.scanQRCode()) } } } } </script> <style scoped> .home-container { display: flex; justify-content: space-around; padding: 20px; min-height: 100vh; background-color: #f5f7fa; } .qr-section { flex: 1; max-width: 500px; margin: 0 20px; } .qr-box { background-color: white; border-radius: 8px; padding: 20px; box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1); } h2 { color: #303133; margin-bottom: 20px; text-align: center; } .qr-content, .scanner-content { display: flex; flex-direction: column; align-items: center; gap: 20px; } .qr-code { width: 237px; height: 237px; display: flex; justify-content: center; align-items: center; border: 1px solid #dcdfe6; border-radius: 4px; } .qr-info { text-align: center; color: #606266; } .scanner { width: 300px; height: 300px; border: 1px solid #dcdfe6; border-radius: 4px; } .scan-result { margin-top: 10px; padding: 10px; background-color: #f0f9eb; color: #67c23a; border-radius: 4px; } .action-button { min-width: 120px; height: 40px; font-size: 16px; border-radius: 4px; transition: all 0.3s; width: 50%; /* 使按钮宽度统一 */ } </style> ``` ## LLM助手 这个界面巨好看保留一下。 ```vue <template> <div class="page-container"> <!-- 顶部按钮区 --> <div class="top-bar"> <el-button type="primary" @click="exportExcel" class="export-btn"> <i class="el-icon-download"></i> 导出销售统计报表 </el-button> </div> <!-- 主要内容区域 --> <div class="main-content"> <!-- 左侧聊天区域 --> <div class="chat-section"> <div class="ai-chat"> <div class="chat-sidebar"> <div class="ai-tools"> <div class="tool-title"> <i class="el-icon-magic-stick"></i> 智能工具箱 </div> <div class="tool-buttons"> <el-button type="text" @click="analyzeData('design')"> <i class="el-icon-brush"></i> 设计趋势分析 </el-button> <el-button type="text" @click="analyzeData('market')"> <i class="el-icon-data-line"></i> 市场数据解读 </el-button> <el-button type="text" @click="analyzeData('production')"> <i class="el-icon-scissors"></i> 生产流程优化 </el-button> <el-button type="text" @click="analyzeData('inventory')"> <i class="el-icon-box"></i> 库存智能预测 </el-button> <el-button type="text" @click="analyzeData('cost')"> <i class="el-icon-money"></i> 成本控制方案 </el-button> </div> </div> </div> <div class="chat-main"> <div class="chat-messages" ref="messageList" style="overflow-y: auto;"> <div v-for="(message, index) in messages" :key="index" :class="['message-item', message.type]"> <div class="message-avatar"> <i :class="message.type === 'user' ? 'el-icon-user' : 'el-icon-cpu'"></i> </div> <div class="message-content"> <div class="message-bubble" v-html="message.text"></div> <div class="message-time"><ruby>message.time }}</div> </div> </div> </div> <div class="chat-input"> <el-input v-model="userInput" type="textarea" <rp> (</rp><rt>rows="2" placeholder="探索时尚前沿,输入您的问题..." resize="none" @keyup.enter.native="sendMessage" > <template slot="prefix"> <i class="el-icon-chat-dot-round"></i> </template> </el-input> <div class="input-actions"> <el-tooltip content="上传图片" placement="top"> <el-button circle icon="el-icon-picture-outline"></el-button> </el-tooltip> <el-tooltip content="发送消息" placement="top"> <el-button type="primary" circle icon="el-icon-s-promotion" :loading="loading" @click="sendMessage" ></el-button> </el-tooltip> </div> </div> </div> </div> </div> <!-- 右侧数据统计面板 --> <div class="stats-panel"> <div class="stats-header"> <h2>实时数据统计</h2> <span class="update-time">更新时间: {{ currentTime</rt><rp>) </rp></ruby></span> </div> <div class="stats-grid"> <!-- 销售数据 --> <div class="stats-card sales"> <div class="card-icon"> <i class="el-icon-money"></i> </div> <div class="card-content"> <h3>今日销售额</h3> <div class="number">¥<ruby>formatNumber(salesData.today) }}</div> <div class="trend"<rp> (</rp><rt>class="salesData.trend >= 0 ? 'up' : 'down'"> <i :class="salesData.trend >= 0 ? 'el-icon-top' : 'el-icon-bottom'"></i> {{ Math.abs(salesData.trend).toFixed(3)</rt><rp>) </rp></ruby>% </div> </div> </div> <!-- 订单数据 --> <div class="stats-card orders"> <div class="card-icon"> <i class="el-icon-s-order"></i> </div> <div class="card-content"> <h3>待处理订单</h3> <div class="number"><ruby>formatNumber(orderData.pending) }}</div> <div class="trend"<rp> (</rp><rt>class="orderData.trend >= 0 ? 'up' : 'down'"> <i :class="orderData.trend >= 0 ? 'el-icon-top' : 'el-icon-bottom'"></i> {{ Math.abs(orderData.trend).toFixed(3)</rt><rp>) </rp></ruby>% </div> </div> </div> <!-- 库存数据 --> <div class="stats-card inventory"> <div class="card-icon"> <i class="el-icon-box"></i> </div> <div class="card-content"> <h3>当前库存</h3> <div class="number"><ruby>formatNumber(inventoryData.current) }}</div> <div class="sub-text">预警值<rp> (</rp><rt>{{ formatNumber(inventoryData.warning)</rt><rp>) </rp></ruby></div> </div> </div> <!-- 生产数据 --> <div class="stats-card production"> <div class="card-icon"> <i class="el-icon-s-cooperation"></i> </div> <div class="card-content"> <h3>生产进度</h3> <el-progress :percentage="productionData.progress" :color="customColors"> </el-progress> <div class="sub-text">预计完成时间: {{ productionData.eta }}</div> </div> </div> </div> <!-- 图表区域 --> <div class="charts-section"> <div class="chart-container"> <h3>销售趋势</h3> <div class="chart" ref="salesChart"></div> </div> </div> </div> </div> </div> </template> <script> import * as echarts from 'echarts'; export default { data() { return { userInput: '', messages: [], loading: false, salesData: { today: 128, // 减小初始值 trend: 12.5 }, orderData: { pending: 56, trend: -5.2 }, inventoryData: { current: 29, warning: 1000 }, productionData: { progress: 66, eta: '2025-01-03 18:00' }, customColors: [ {color: '#f56c6c', percentage: 20}, {color: '#e6a23c', percentage: 40}, {color: '#5cb87a', percentage: 60}, {color: '#1989fa', percentage: 80}, {color: '#6f7ad3', percentage: 100} ], salesChart: null, controller: null, systemPrompt: `你是一个专业的服装生产销售系统助手,具备以下专业知识和能力: 1. 服装生产流程:了解从面料选择到成品出厂的完整生产流程 2. 质量控制:熟悉服装质量标准和检验方法 3. 供应链管理:了解原材料采购、库存管理和物流配送 4. 销售策略:掌握服装市场营销和销售技巧 5. 成本控制:能够分析和优化生产成本 6. 时尚趋势:了解当前市场趋势和消费者偏好 7. 数据分析:能够解读销售数据和市场数据 8. 尺码建议:能够根据顾客的身高体重等信息提供准确的尺码建议` }; }, computed: { currentTime() { return new Date().toLocaleTimeString(); } }, mounted() { this.initSalesChart(); this.startDataRefresh(); this.initMessages(); }, methods: { async exportExcel() { try { // 调用后端API获取数据 const response = await this.$http.get('/sales/export'); // 使用 Excel.js 或其他库处理导出 console.log('导出销售统计报表', response); this.$message.success('报表导出成功'); } catch (error) { this.$message.error('报表导出失败'); console.error('导出失败:', error); } }, formatNumber(num) { // 先保留两位小数,然后添加千分位分隔符 return Number(num).toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ","); }, initSalesChart() { this.salesChart = echarts.init(this.$refs.salesChart); const option = { tooltip: { trigger: 'axis' }, grid: { top: '10%', left: '3%', right: '4%', bottom: '3%', containLabel: true }, xAxis: { type: 'category', data: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'], axisLine: { lineStyle: { color: '#8d8d8d' } } }, yAxis: { type: 'value', axisLine: { lineStyle: { color: '#8d8d8d' } }, splitLine: { lineStyle: { color: 'rgba(255, 255, 255, 0.1)' } } }, series: [{ data: [820, 932, 901, 934, 1290, 1330, 1320], type: 'line', smooth: true, symbolSize: 8, itemStyle: { color: '#ff2e63' }, areaStyle: { color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{ offset: 0, color: 'rgba(255, 46, 99, 0.3)' }, { offset: 1, color: 'rgba(255, 46, 99, 0.1)' }]) } }] }; this.salesChart.setOption(option); }, startDataRefresh() { setInterval(() => { // 使用Math.floor生成整数 this.salesData.today += Math.floor(Math.random() * 10) - 5; // 生成-5到4之间的整数 this.salesData.trend = Number((Math.random() * 20 - 10).toFixed(3)); // 限制小数位数 this.orderData.pending += Math.floor(Math.random() * 3) - 1; // 生成-1到1之间的整数 this.orderData.trend = Number((Math.random() * 10 - 5).toFixed(3)); // 限制小数位数 this.inventoryData.current += Math.floor(Math.random() * 10) - 2; }, 5000); }, initMessages() { // 初始化一条欢迎消息 this.messages.push({ type: 'bot', text: '您好!我是您的智能助手,请问有什么可以帮您?', time: this.currentTime }); }, async sendMessage() { if (!this.userInput.trim() || this.loading) return; const now = new Date() const time = `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}` // Add user message this.messages.push({ type: 'user', text: this.userInput.replace(/\n/g, '<br>'), time }) // Prepare bot message const botMessage = { type: 'bot', text: '', time, streaming: true } this.messages.push(botMessage) // Clear input and scroll to bottom const currentInput = this.userInput this.userInput = '' this.$nextTick(() => { const messageList = this.$refs.messageList; messageList.scrollTop = messageList.scrollHeight; }) this.loading = true try { this.controller = new AbortController() const response = await fetch('https://api.openai-hk.com/v1/chat/completions', { method: 'POST', headers: { 'Authorization': 'Bearer hk-wod5ai1000049605c747e2eddc162609a94d321f740f8f19', 'Content-Type': 'application/json' }, body: JSON.stringify({ model: 'gpt-3.5-turbo', messages: [ { role: 'system', content: this.systemPrompt }, { role: 'user', content: currentInput } ], max_tokens: 1200, temperature: 0.8, top_p: 1, presence_penalty: 1, stream: true }), signal: this.controller.signal }) const reader = response.body.getReader() const decoder = new TextDecoder() while (true) { const { done, value } = await reader.read() if (done) break const chunk = decoder.decode(value) const lines = chunk.split('\n') for (const line of lines) { if (line.startsWith('data: ') && line !== 'data: [DONE]') { try { const data = JSON.parse(line.slice(6)) if (data.choices[0]?.delta?.content) { botMessage.text += data.choices[0].delta.content this.$nextTick(() => { const messageList = this.$refs.messageList; messageList.scrollTop = messageList.scrollHeight; }) } } catch (e) { console.error('Error parsing stream:', e) } } } } } catch (error) { if (error.name === 'AbortError') { console.log('Request aborted') } else { console.error('Error fetching response:', error) botMessage.text = '抱歉,我现在无法处理您的请求。' } } finally { botMessage.streaming = false this.loading = false this.controller = null } }, async askQuickQuestion(question) { if (this.loading) return this.userInput = question await this.sendMessage() }, startChat() { this.messages.push({ type: 'bot', text: '您好!我是您的智能助手,请问有什么可以帮您?', time: this.currentTime }); }, getCurrentTime() { const now = new Date() return `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}` }, scrollToBottom() { const messageList = this.$refs.messageList if (messageList) { messageList.scrollTop = messageList.scrollHeight } }, scrollToTop() { const messageList = this.$refs.messageList if (messageList) { messageList.scrollTo({ top: 0, behavior: 'smooth' }) } }, handleScroll() { const messageList = this.$refs.messageList this.showScrollTop = messageList.scrollTop > 200 }, async analyzeData(type) { const analysisPrompts = { design: '请分析当前服装设计趋势,包括流行元素、色彩搭配、款式创新等方面。', market: '请解读当前市场数据,包括销售热点、消费者偏好、竞品分析等方面。', production: '请优化生产流程,包括工艺改进、效率提升、质量控制等方面。', inventory: '请预测库存趋势,包括畅销款补货、滞销款处理、季节性储备等方面。', cost: '请提供成本控制方案,包括材料采购、人工优化、费用节约等方面。' }; if (this.loading) return; const prompt = analysisPrompts[type]; if (prompt) { this.userInput = prompt; await this.sendMessage(); } }, async getAIResponse(message) { try { // 创建AbortController用于取消请求 this.controller = new AbortController(); const response = await this.$http({ url: '/renshi/chat', // 修改为后端API地址 method: 'post', data: { message: message, systemPrompt: this.systemPrompt } }); // 检查响应状态 if (response.data.code !== 0) { throw new Error(response.data.msg || '请求失败'); } // 添加AI回复消息 this.messages.push({ type: 'bot', text: response.data.data || response.data.msg, time: this.currentTime }); // 滚动到底部 this.$nextTick(() => { const messageList = this.$refs.messageList; messageList.scrollTop = messageList.scrollHeight; }); } catch (error) { console.error('AI响应错误:', error); this.messages.push({ type: 'bot', text: '抱歉,系统暂时无法处理您的请求,请稍后再试。', time: this.currentTime }); throw error; } finally { this.controller = null; } }, } } </script> <style scoped> .page-container { height: calc(100vh - 120px); background: #1a1a2e; color: #fff; padding: 20px; display: flex; flex-direction: column; } .main-content { display: grid; grid-template-columns: 1fr 400px; gap: 20px; height: 100%; flex: 1; min-height: 0; /* 重要:允许内容收缩 */ } .chat-section { background: rgba(255, 255, 255, 0.05); border-radius: 15px; overflow: hidden; display: flex; flex-direction: column; height: 100%; } .ai-chat { height: 100%; display: grid; grid-template-columns: 300px 1fr; background: transparent; } .chat-main { display: flex; flex-direction: column; height: 100%; min-height: 0; /* 重要:允许内容收缩 */ } .chat-messages { flex: 1; overflow-y: auto; padding: 20px; scrollbar-width: thin; scrollbar-color: rgba(255, 255, 255, 0.2) transparent; min-height: 0; /* 重要:允许内容收缩 */ } .chat-input { padding: 15px; background: rgba(255, 255, 255, 0.05); margin-top: auto; /* 将输入框固定在底部 */ } .stats-panel { background: rgba(255, 255, 255, 0.05); border-radius: 15px; padding: 20px; } .stats-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; } .stats-header h2 { margin: 0; font-size: 20px; background: linear-gradient(135deg, #fff 0%, #e2e2e2 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; } .update-time { font-size: 12px; color: #8d8d8d; } .stats-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 15px; margin-bottom: 20px; } .stats-card { background: rgba(255, 255, 255, 0.05); border-radius: 12px; padding: 15px; display: flex; gap: 15px; } .card-icon { width: 40px; height: 40px; border-radius: 10px; display: flex; align-items: center; justify-content: center; font-size: 20px; } .stats-card.sales .card-icon { background: linear-gradient(135deg, #ff2e63 0%, #ff0844 100%); } .stats-card.orders .card-icon { background: linear-gradient(135deg, #00b4d8 0%, #0077b6 100%); } .stats-card.inventory .card-icon { background: linear-gradient(135deg, #7209b7 0%, #560bad 100%); } .stats-card.production .card-icon { background: linear-gradient(135deg, #4cc9f0 0%, #4895ef 100%); } .card-content h3 { margin: 0 0 5px; font-size: 14px; color: #8d8d8d; } .number { font-size: 24px; font-weight: bold; margin-bottom: 5px; } .trend { font-size: 12px; display: flex; align-items: center; gap: 4px; } .trend.up { color: #67c23a; } .trend.down { color: #f56c6c; } .sub-text { font-size: 12px; color: #8d8d8d; } .charts-section { margin-top: 20px; } .chart-container { background: rgba(255, 255, 255, 0.05); border-radius: 12px; padding: 15px; } .chart-container h3 { margin: 0 0 15px; font-size: 16px; color: #8d8d8d; } .chart { height: 180px; } /* 聊天界面样式 */ .ai-chat { height: 100%; display: grid; grid-template-columns: 300px 1fr; background: transparent; } .chat-sidebar { padding: 20px; border-right: 1px solid rgba(255, 255, 255, 0.1); } .chat-main { display: flex; flex-direction: column; } .chat-messages { flex: 1; overflow-y: auto; padding: 20px; scrollbar-width: thin; scrollbar-color: rgba(255, 255, 255, 0.2) transparent; } .chat-messages::-webkit-scrollbar { width: 6px; } .chat-messages::-webkit-scrollbar-track { background: transparent; } .chat-messages::-webkit-scrollbar-thumb { background-color: rgba(255, 255, 255, 0.2); border-radius: 3px; } .chat-messages::-webkit-scrollbar-thumb:hover { background-color: rgba(255, 255, 255, 0.3); } .message-item { display: flex; gap: 15px; margin-bottom: 20px; } .message-item.user { flex-direction: row-reverse; } .message-avatar { width: 40px; height: 40px; background: rgba(255, 255, 255, 0.1); border-radius: 12px; display: flex; align-items: center; justify-content: center; } .message-avatar i { font-size: 20px; } .message-bubble { background: rgba(255, 255, 255, 0.05); padding: 15px 20px; border-radius: 15px; max-width: 70%; } .message-item.user .message-bubble { background: linear-gradient(135deg, #ff2e63 0%, #ff0844 100%); } .message-time { font-size: 12px; color: #8d8d8d; margin-top: 5px; } .chat-input { padding: 15px; background: rgba(255, 255, 255, 0.05); margin-top: auto; /* 将输入框固定在底部 */ } .input-actions { display: flex; justify-content: flex-end; gap: 10px; margin-top: 10px; } /* Element UI 组件样式覆盖 */ :deep(.el-progress-bar__outer) { background-color: rgba(255, 255, 255, 0.1) !important; } :deep(.el-progress-bar__inner) { transition: all 0.3s ease; } :deep(.el-input__inner), :deep(.el-textarea__inner) { background: rgba(255, 255, 255, 0.05); border: none; color: #fff; } :deep(.el-button--primary) { background: linear-gradient(135deg, #ff2e63 0%, #ff0844 100%); border: none; } .tool-title { font-size: 16px; margin-bottom: 15px; display: flex; align-items: center; color: #ff2e63; } .tool-title i { margin-right: 8px; } .tool-buttons { display: flex; flex-direction: column; gap: 8px; } .tool-buttons .el-button { text-align: left; color: #fff; padding: 8px; } .tool-buttons .el-button i { margin-right: 8px; font-size: 16px; } </style> ``` # 总结 最后这篇文章是回忆所写,所以修改的部分可能还有些细微记不太清,但是目前项目能运行就行。这个修改只是给了一个参考思路,可以以后做一些后续修改,希望有启发。 最后修改:2025 年 01 月 20 日 © 允许规范转载 打赏 赞赏作者 支付宝微信 赞 2 如果觉得我的文章对你有用,请随意赞赏
2 条评论
思想的火花在字句间迸发,照亮认知盲区。
建议后续持续追踪此话题,形成系列研究。