yyw提交

This commit is contained in:
DESKTOP-8FGKA8Q\chunfen 2025-06-14 10:30:10 +08:00
parent 0ed1e71f00
commit 33a9f5c761
50 changed files with 37466 additions and 10 deletions

3
.browserslistrc Normal file
View File

@ -0,0 +1,3 @@
> 1%
last 2 versions
not dead

19
.eslintrc.js Normal file
View File

@ -0,0 +1,19 @@
module.exports = {
root: true,
env: {
node: true
},
'extends': [
'plugin:vue/vue3-essential',
'eslint:recommended'
],
parserOptions: {
parser: 'babel-eslint'
},
rules: {
// 'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
// 'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off'
'no-console': 'off',
'no-debugger': 'off'
}
}

27
.gitignore vendored
View File

@ -1,11 +1,22 @@
# ---> Vue
# gitignore template for Vue.js projects
#
# Recommended template: Node.gitignore
.DS_Store
node_modules
/dist
# TODO: where does this rule come from?
docs/_book
# local env files
.env.local
.env.*.local
# TODO: where does this rule come from?
test/
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@ -1,2 +0,0 @@
# myERP

14
babel.config.js Normal file
View File

@ -0,0 +1,14 @@
// 项目在发布时需要用到的 babel 插件数组
const proPlugins = [];
// 如果当前是测试环境或者是生产环境,则使用去掉 console 的插件
if (process.env.NODE_ENV === 'test' || process.env.NODE_ENV === 'production') {
proPlugins.push('transform-remove-console');
}
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
],
plugins:[
...proPlugins
]
}

14147
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

54
package.json Normal file
View File

@ -0,0 +1,54 @@
{
"name": "e-commerce",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
"dependencies": {
"@element-plus/icons": "0.0.11",
"@wecom/jssdk": "^2.0.2",
"axios": "^0.24.0",
"bignumber.js": "^9.1.2",
"canvas": "^2.9.0",
"clipboard": "^2.0.11",
"core-js": "^3.6.5",
"dayjs": "^1.11.13",
"echarts": "^5.3.2",
"element-plus": "^2.8.4",
"file-saver": "^2.0.5",
"jquery": "^3.6.0",
"laravel-echo": "^1.11.7",
"moment": "^2.29.4",
"pusher-js": "^7.1.0-beta",
"sass": "^1.43.4",
"sortablejs": "^1.15.0",
"vue": "^3.0.0",
"vue-router": "^4.0.0-0",
"vue3-treeselect": "^0.1.10",
"vuex": "^4.1.0",
"wangeditor": "^4.7.9",
"xe-utils": "^3.5.7",
"xlsx": "^0.17.4",
"xlsx-style": "^0.8.13"
},
"devDependencies": {
"@arco-design/web-vue": "^2.55.2",
"@vue/cli-plugin-babel": "~4.5.0",
"@vue/cli-plugin-eslint": "~4.5.0",
"@vue/cli-plugin-router": "~4.5.0",
"@vue/cli-plugin-vuex": "~4.5.0",
"@vue/cli-service": "~4.5.0",
"@vue/compiler-sfc": "^3.0.0",
"babel-eslint": "^10.1.0",
"babel-plugin-transform-remove-console": "^6.9.4",
"eslint": "^6.7.2",
"eslint-plugin-vue": "^7.0.0",
"node-sass": "^4.14.1",
"sass-loader": "^8.0.2",
"script-loader": "^0.7.2",
"webpack": "^4.46.0"
}
}

20
public/index.html Normal file
View File

@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<meta http-equiv="pragram" content="no-cache">
<meta http-equiv="cache-control" content="no-cache, no-store, must-revalidate">
<meta http-equiv="expires" content="0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>

74
src/App.vue Normal file
View File

@ -0,0 +1,74 @@
<template>
<router-view />
</template>
<script src="@/assets/icon/iconfont.js"></script>
<style lang="scss">
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
html,
body {
margin: 0;
padding: 0;
}
/*解决element表格不对齐*/
body .el-table th.gutter {
display: table-cell !important;
}
.page-pagination {
width: 100%;
display: flex;
justify-content: center;
background-color: #fff;
padding-top: 15px;
}
.scrollbar-wrapper {
overflow-x: hidden !important;
}
/*解决element上传组件回显动画问题*/
body .el-upload-list__item {
transition: none !important;
}
.pageBox{
// height: 100%;
// overflow: auto;
padding: 15px;
box-sizing: border-box;
}
body .el-message-box__title{
font-size: 16px;
}
body .el-page-header__back{
border: 1px solid #D9D9D9;
border-radius: 4px;
padding: 2px 8px;
}
/* 设置滚动条的样式 */
::-webkit-scrollbar {
width: 7px;
height: 7px;
}
/* 滚动槽 */
::-webkit-scrollbar-track {
box-shadow: inset 0 0 5px rgba(0,0,0,0.2);
border-radius: 0;
background: rgba(0,0,0,0.1);
}
/* 滚动条滑块 */
::-webkit-scrollbar-thumb {
border-radius: 4px;
box-shadow: inset 0 0 5px rgba(0,0,0,0.2);
background: rgba(0,0,0,0.2);
}
.vanMsg.el-message{
left: 60px;
transform: none;
}
</style>

4
src/api/index.js Normal file
View File

@ -0,0 +1,4 @@
const url = "http://shop.dev.chutang66.com"; // 测试
export default {
url,
};

157
src/api/request.js Normal file
View File

@ -0,0 +1,157 @@
import axios from 'axios'
import router from '@/router'
import { ElMessage } from 'element-plus'
let showMsg = true;
function interceptRequest () {
axios.interceptors.request.use(config =>
{
config.headers['content-type'] = 'application/json'
config.headers.Authorization = localStorage.getItem('token') || ''
config.headers['Shop-Id'] = localStorage.getItem('shopId') || ''
// 登录接口、登出接口、刷新token接口不需要token
if (config.url.indexOf('/auth/refresh') >= 0 || config.url.indexOf('/auth/login') >= 0 || config.url.indexOf('/auth/logout') >= 0) {
return config
}
let access_token = localStorage.getItem('token')
let saveTime = Number(localStorage.getItem('saveTime'))
let expires_in = Number(localStorage.getItem('expires_in'))
// 如果token已经过期
if((access_token && saveTime && expires_in && (Date.now() > saveTime + expires_in)) || !access_token) {
ElMessage('登录用户已过期,请重新登录')
localStorage.removeItem('token')
localStorage.removeItem('saveTime')
localStorage.removeItem('bind_wechat')
router.replace({
path: '/login'
})
Reflect.deleteProperty(config, 'headers')
return config
}
return config
}, err => {
return Promise.reject(err)
})
axios.interceptors.response.use(response => {
if (response.status === 200 || response.status === 201 || response.status === 204) {
return response
}
// return response
}, err => {
return Promise.reject(err)
})
}
/**
* get方法对应get请求
* @param {String} url [请求的url地址]
* @param {Object} params [请求时携带的参数]
*/
export function get (url = '', params = {}, responseType = '') {
return new Promise((resolve, reject) => {
interceptRequest()
axios
.get(url, {
params: params,
responseType
})
.then(res => {
resolve(res.data)
})
.catch(err => {
if (showMsg && err.response && err.response.data && err.response.data.message) {
ElMessage(err.response.data.message)
}
reject(err)
})
})
}
/**
* post方法对应post请求
* @param {String} url [请求的url地址]
* @param {Object} data [请求时携带的参数]
* @param { string } requestType 请求类型 默认为post
*/
export function post (url = '', data = {}, requestType = 'post') {
requestType = requestType.toLowerCase()
return new Promise((resolve, reject) => {
interceptRequest()
let dataObj = {}
dataObj = requestType === 'delete' ? { data } : data
axios[requestType](url, { ...dataObj })
.then(res => {
resolve(res.data)
})
.catch(err => {
if (showMsg && err.response && err.response.data && err.response.data.message) {
ElMessage(err.response.data.message)
}
reject(err)
})
})
}
/**
* post方法用于处理图片等文件流
* @param {String} url [请求的url地址]
* @param {Object} data [请求时携带的参数]
* @param { string } requestType 请求类型 默认为post
*/
export function postForm(url = '', data = {}, requestType = 'post') {
const formData = new FormData();
Object.keys(data).forEach(key => {
const item = data[key]
if (key === 'goods') {
return
}
if (Array.isArray(data[key])) {
if (key === 'specs') {
formData.append(key, JSON.stringify(item));
return
}
if (item.length) {
item.forEach( (j, index) => {
if (Object.prototype.toString.call(j) === '[object Object]') {
Object.keys(j).forEach(jkey => {
formData.append(`${key}[${index}][${jkey}]`, j[jkey]);
})
} else {
formData.append(`${key}[]`, j || '');
}
})
} else {
formData.append(`${key}`, []);
}
} else if (Object.prototype.toString.call(item) === '[object Object]') {
Object.keys(item).forEach(jkey => {
formData.append(`${key}[${jkey}]`, item[jkey]);
})
} else {
if(data[key] === 0) {
formData.append(key, data[key]);
} else {
formData.append(key, data[key] || '');
}
}
})
return new Promise((resolve, reject) => {
interceptRequest()
axios[requestType](url, formData)
.then(res => {
resolve(res.data)
})
.catch(err => {
ElMessage({
type: 'warning',
message: err.response.data.message
})
reject(err)
})
})
}

16077
src/assets/citylist.js Normal file

File diff suppressed because it is too large Load Diff

539
src/assets/icon/demo.css Normal file
View File

@ -0,0 +1,539 @@
/* Logo 字体 */
@font-face {
font-family: "iconfont logo";
src: url('https://at.alicdn.com/t/font_985780_km7mi63cihi.eot?t=1545807318834');
src: url('https://at.alicdn.com/t/font_985780_km7mi63cihi.eot?t=1545807318834#iefix') format('embedded-opentype'),
url('https://at.alicdn.com/t/font_985780_km7mi63cihi.woff?t=1545807318834') format('woff'),
url('https://at.alicdn.com/t/font_985780_km7mi63cihi.ttf?t=1545807318834') format('truetype'),
url('https://at.alicdn.com/t/font_985780_km7mi63cihi.svg?t=1545807318834#iconfont') format('svg');
}
.logo {
font-family: "iconfont logo";
font-size: 160px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* tabs */
.nav-tabs {
position: relative;
}
.nav-tabs .nav-more {
position: absolute;
right: 0;
bottom: 0;
height: 42px;
line-height: 42px;
color: #666;
}
#tabs {
border-bottom: 1px solid #eee;
}
#tabs li {
cursor: pointer;
width: 100px;
height: 40px;
line-height: 40px;
text-align: center;
font-size: 16px;
border-bottom: 2px solid transparent;
position: relative;
z-index: 1;
margin-bottom: -1px;
color: #666;
}
#tabs .active {
border-bottom-color: #f00;
color: #222;
}
.tab-container .content {
display: none;
}
/* 页面布局 */
.main {
padding: 30px 100px;
width: 960px;
margin: 0 auto;
}
.main .logo {
color: #333;
text-align: left;
margin-bottom: 30px;
line-height: 1;
height: 110px;
margin-top: -50px;
overflow: hidden;
*zoom: 1;
}
.main .logo a {
font-size: 160px;
color: #333;
}
.helps {
margin-top: 40px;
}
.helps pre {
padding: 20px;
margin: 10px 0;
border: solid 1px #e7e1cd;
background-color: #fffdef;
overflow: auto;
}
.icon_lists {
width: 100% !important;
overflow: hidden;
*zoom: 1;
}
.icon_lists li {
width: 100px;
margin-bottom: 10px;
margin-right: 20px;
text-align: center;
list-style: none !important;
cursor: default;
}
.icon_lists li .code-name {
line-height: 1.2;
}
.icon_lists .icon {
display: block;
height: 100px;
line-height: 100px;
font-size: 42px;
margin: 10px auto;
color: #333;
-webkit-transition: font-size 0.25s linear, width 0.25s linear;
-moz-transition: font-size 0.25s linear, width 0.25s linear;
transition: font-size 0.25s linear, width 0.25s linear;
}
.icon_lists .icon:hover {
font-size: 100px;
}
.icon_lists .svg-icon {
/* 通过设置 font-size 来改变图标大小 */
width: 1em;
/* 图标和文字相邻时,垂直对齐 */
vertical-align: -0.15em;
/* 通过设置 color 来改变 SVG 的颜色/fill */
fill: currentColor;
/* path stroke 溢出 viewBox 部分在 IE 下会显示
normalize.css 中也包含这行 */
overflow: hidden;
}
.icon_lists li .name,
.icon_lists li .code-name {
color: #666;
}
/* markdown 样式 */
.markdown {
color: #666;
font-size: 14px;
line-height: 1.8;
}
.highlight {
line-height: 1.5;
}
.markdown img {
vertical-align: middle;
max-width: 100%;
}
.markdown h1 {
color: #404040;
font-weight: 500;
line-height: 40px;
margin-bottom: 24px;
}
.markdown h2,
.markdown h3,
.markdown h4,
.markdown h5,
.markdown h6 {
color: #404040;
margin: 1.6em 0 0.6em 0;
font-weight: 500;
clear: both;
}
.markdown h1 {
font-size: 28px;
}
.markdown h2 {
font-size: 22px;
}
.markdown h3 {
font-size: 16px;
}
.markdown h4 {
font-size: 14px;
}
.markdown h5 {
font-size: 12px;
}
.markdown h6 {
font-size: 12px;
}
.markdown hr {
height: 1px;
border: 0;
background: #e9e9e9;
margin: 16px 0;
clear: both;
}
.markdown p {
margin: 1em 0;
}
.markdown>p,
.markdown>blockquote,
.markdown>.highlight,
.markdown>ol,
.markdown>ul {
width: 80%;
}
.markdown ul>li {
list-style: circle;
}
.markdown>ul li,
.markdown blockquote ul>li {
margin-left: 20px;
padding-left: 4px;
}
.markdown>ul li p,
.markdown>ol li p {
margin: 0.6em 0;
}
.markdown ol>li {
list-style: decimal;
}
.markdown>ol li,
.markdown blockquote ol>li {
margin-left: 20px;
padding-left: 4px;
}
.markdown code {
margin: 0 3px;
padding: 0 5px;
background: #eee;
border-radius: 3px;
}
.markdown strong,
.markdown b {
font-weight: 600;
}
.markdown>table {
border-collapse: collapse;
border-spacing: 0px;
empty-cells: show;
border: 1px solid #e9e9e9;
width: 95%;
margin-bottom: 24px;
}
.markdown>table th {
white-space: nowrap;
color: #333;
font-weight: 600;
}
.markdown>table th,
.markdown>table td {
border: 1px solid #e9e9e9;
padding: 8px 16px;
text-align: left;
}
.markdown>table th {
background: #F7F7F7;
}
.markdown blockquote {
font-size: 90%;
color: #999;
border-left: 4px solid #e9e9e9;
padding-left: 0.8em;
margin: 1em 0;
}
.markdown blockquote p {
margin: 0;
}
.markdown .anchor {
opacity: 0;
transition: opacity 0.3s ease;
margin-left: 8px;
}
.markdown .waiting {
color: #ccc;
}
.markdown h1:hover .anchor,
.markdown h2:hover .anchor,
.markdown h3:hover .anchor,
.markdown h4:hover .anchor,
.markdown h5:hover .anchor,
.markdown h6:hover .anchor {
opacity: 1;
display: inline-block;
}
.markdown>br,
.markdown>p>br {
clear: both;
}
.hljs {
display: block;
background: white;
padding: 0.5em;
color: #333333;
overflow-x: auto;
}
.hljs-comment,
.hljs-meta {
color: #969896;
}
.hljs-string,
.hljs-variable,
.hljs-template-variable,
.hljs-strong,
.hljs-emphasis,
.hljs-quote {
color: #df5000;
}
.hljs-keyword,
.hljs-selector-tag,
.hljs-type {
color: #a71d5d;
}
.hljs-literal,
.hljs-symbol,
.hljs-bullet,
.hljs-attribute {
color: #0086b3;
}
.hljs-section,
.hljs-name {
color: #63a35c;
}
.hljs-tag {
color: #333333;
}
.hljs-title,
.hljs-attr,
.hljs-selector-id,
.hljs-selector-class,
.hljs-selector-attr,
.hljs-selector-pseudo {
color: #795da3;
}
.hljs-addition {
color: #55a532;
background-color: #eaffea;
}
.hljs-deletion {
color: #bd2c00;
background-color: #ffecec;
}
.hljs-link {
text-decoration: underline;
}
/* 代码高亮 */
/* PrismJS 1.15.0
https://prismjs.com/download.html#themes=prism&languages=markup+css+clike+javascript */
/**
* prism.js default theme for JavaScript, CSS and HTML
* Based on dabblet (http://dabblet.com)
* @author Lea Verou
*/
code[class*="language-"],
pre[class*="language-"] {
color: black;
background: none;
text-shadow: 0 1px white;
font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
text-align: left;
white-space: pre;
word-spacing: normal;
word-break: normal;
word-wrap: normal;
line-height: 1.5;
-moz-tab-size: 4;
-o-tab-size: 4;
tab-size: 4;
-webkit-hyphens: none;
-moz-hyphens: none;
-ms-hyphens: none;
hyphens: none;
}
pre[class*="language-"]::-moz-selection,
pre[class*="language-"] ::-moz-selection,
code[class*="language-"]::-moz-selection,
code[class*="language-"] ::-moz-selection {
text-shadow: none;
background: #b3d4fc;
}
pre[class*="language-"]::selection,
pre[class*="language-"] ::selection,
code[class*="language-"]::selection,
code[class*="language-"] ::selection {
text-shadow: none;
background: #b3d4fc;
}
@media print {
code[class*="language-"],
pre[class*="language-"] {
text-shadow: none;
}
}
/* Code blocks */
pre[class*="language-"] {
padding: 1em;
margin: .5em 0;
overflow: auto;
}
:not(pre)>code[class*="language-"],
pre[class*="language-"] {
background: #f5f2f0;
}
/* Inline code */
:not(pre)>code[class*="language-"] {
padding: .1em;
border-radius: .3em;
white-space: normal;
}
.token.comment,
.token.prolog,
.token.doctype,
.token.cdata {
color: slategray;
}
.token.punctuation {
color: #999;
}
.namespace {
opacity: .7;
}
.token.property,
.token.tag,
.token.boolean,
.token.number,
.token.constant,
.token.symbol,
.token.deleted {
color: #905;
}
.token.selector,
.token.attr-name,
.token.string,
.token.char,
.token.builtin,
.token.inserted {
color: #690;
}
.token.operator,
.token.entity,
.token.url,
.language-css .token.string,
.style .token.string {
color: #9a6e3a;
background: hsla(0, 0%, 100%, .5);
}
.token.atrule,
.token.attr-value,
.token.keyword {
color: #07a;
}
.token.function,
.token.class-name {
color: #DD4A68;
}
.token.regex,
.token.important,
.token.variable {
color: #e90;
}
.token.important,
.token.bold {
font-weight: bold;
}
.token.italic {
font-style: italic;
}
.token.entity {
cursor: help;
}

View File

@ -0,0 +1,441 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<title>iconfont Demo</title>
<link rel="shortcut icon" href="//img.alicdn.com/imgextra/i4/O1CN01Z5paLz1O0zuCC7osS_!!6000000001644-55-tps-83-82.svg" type="image/x-icon"/>
<link rel="icon" type="image/svg+xml" href="//img.alicdn.com/imgextra/i4/O1CN01Z5paLz1O0zuCC7osS_!!6000000001644-55-tps-83-82.svg"/>
<link rel="stylesheet" href="https://g.alicdn.com/thx/cube/1.3.2/cube.min.css">
<link rel="stylesheet" href="demo.css">
<link rel="stylesheet" href="iconfont.css">
<script src="iconfont.js"></script>
<!-- jQuery -->
<script src="https://a1.alicdn.com/oss/uploads/2018/12/26/7bfddb60-08e8-11e9-9b04-53e73bb6408b.js"></script>
<!-- 代码高亮 -->
<script src="https://a1.alicdn.com/oss/uploads/2018/12/26/a3f714d0-08e6-11e9-8a15-ebf944d7534c.js"></script>
<style>
.main .logo {
margin-top: 0;
height: auto;
}
.main .logo a {
display: flex;
align-items: center;
}
.main .logo .sub-title {
margin-left: 0.5em;
font-size: 22px;
color: #fff;
background: linear-gradient(-45deg, #3967FF, #B500FE);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
</style>
</head>
<body>
<div class="main">
<h1 class="logo"><a href="https://www.iconfont.cn/" title="iconfont 首页" target="_blank">
<img width="200" src="https://img.alicdn.com/imgextra/i3/O1CN01Mn65HV1FfSEzR6DKv_!!6000000000514-55-tps-228-59.svg">
</a></h1>
<div class="nav-tabs">
<ul id="tabs" class="dib-box">
<li class="dib active"><span>Unicode</span></li>
<li class="dib"><span>Font class</span></li>
<li class="dib"><span>Symbol</span></li>
</ul>
<a href="https://www.iconfont.cn/manage/index?manage_type=myprojects&projectId=4742714" target="_blank" class="nav-more">查看项目</a>
</div>
<div class="tab-container">
<div class="content unicode" style="display: block;">
<ul class="icon_lists dib-box">
<li class="dib">
<span class="icon iconfont">&#xe679;</span>
<div class="name">组合</div>
<div class="code-name">&amp;#xe679;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe77e;</span>
<div class="name">首页</div>
<div class="code-name">&amp;#xe77e;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe685;</span>
<div class="name">商品列表</div>
<div class="code-name">&amp;#xe685;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe854;</span>
<div class="name">导出</div>
<div class="code-name">&amp;#xe854;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe664;</span>
<div class="name">导入</div>
<div class="code-name">&amp;#xe664;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe722;</span>
<div class="name">品牌</div>
<div class="code-name">&amp;#xe722;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe61d;</span>
<div class="name">采购单</div>
<div class="code-name">&amp;#xe61d;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe668;</span>
<div class="name">仓库</div>
<div class="code-name">&amp;#xe668;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe611;</span>
<div class="name">供应商</div>
<div class="code-name">&amp;#xe611;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe65b;</span>
<div class="name">商品规格</div>
<div class="code-name">&amp;#xe65b;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe600;</span>
<div class="name">出入库</div>
<div class="code-name">&amp;#xe600;</div>
</li>
</ul>
<div class="article markdown">
<h2 id="unicode-">Unicode 引用</h2>
<hr>
<p>Unicode 是字体在网页端最原始的应用方式,特点是:</p>
<ul>
<li>支持按字体的方式去动态调整图标大小,颜色等等。</li>
<li>默认情况下不支持多色,直接添加多色图标会自动去色。</li>
</ul>
<blockquote>
<p>注意:新版 iconfont 支持两种方式引用多色图标SVG symbol 引用方式和彩色字体图标模式。(使用彩色字体图标需要在「编辑项目」中开启「彩色」选项后并重新生成。)</p>
</blockquote>
<p>Unicode 使用步骤如下:</p>
<h3 id="-font-face">第一步:拷贝项目下面生成的 <code>@font-face</code></h3>
<pre><code class="language-css"
>@font-face {
font-family: 'iconfont';
src: url('iconfont.woff2?t=1731383577261') format('woff2'),
url('iconfont.woff?t=1731383577261') format('woff'),
url('iconfont.ttf?t=1731383577261') format('truetype');
}
</code></pre>
<h3 id="-iconfont-">第二步:定义使用 iconfont 的样式</h3>
<pre><code class="language-css"
>.iconfont {
font-family: "iconfont" !important;
font-size: 16px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
</code></pre>
<h3 id="-">第三步:挑选相应图标并获取字体编码,应用于页面</h3>
<pre>
<code class="language-html"
>&lt;span class="iconfont"&gt;&amp;#x33;&lt;/span&gt;
</code></pre>
<blockquote>
<p>"iconfont" 是你项目下的 font-family。可以通过编辑项目查看默认是 "iconfont"。</p>
</blockquote>
</div>
</div>
<div class="content font-class">
<ul class="icon_lists dib-box">
<li class="dib">
<span class="icon iconfont icon-cc"></span>
<div class="name">
组合
</div>
<div class="code-name">.icon-cc
</div>
</li>
<li class="dib">
<span class="icon iconfont icon-home"></span>
<div class="name">
首页
</div>
<div class="code-name">.icon-home
</div>
</li>
<li class="dib">
<span class="icon iconfont icon-list"></span>
<div class="name">
商品列表
</div>
<div class="code-name">.icon-list
</div>
</li>
<li class="dib">
<span class="icon iconfont icon-daochu"></span>
<div class="name">
导出
</div>
<div class="code-name">.icon-daochu
</div>
</li>
<li class="dib">
<span class="icon iconfont icon-daoru"></span>
<div class="name">
导入
</div>
<div class="code-name">.icon-daoru
</div>
</li>
<li class="dib">
<span class="icon iconfont icon-brand"></span>
<div class="name">
品牌
</div>
<div class="code-name">.icon-brand
</div>
</li>
<li class="dib">
<span class="icon iconfont icon-caigou"></span>
<div class="name">
采购单
</div>
<div class="code-name">.icon-caigou
</div>
</li>
<li class="dib">
<span class="icon iconfont icon-cangku"></span>
<div class="name">
仓库
</div>
<div class="code-name">.icon-cangku
</div>
</li>
<li class="dib">
<span class="icon iconfont icon-supplier"></span>
<div class="name">
供应商
</div>
<div class="code-name">.icon-supplier
</div>
</li>
<li class="dib">
<span class="icon iconfont icon-sku"></span>
<div class="name">
商品规格
</div>
<div class="code-name">.icon-sku
</div>
</li>
<li class="dib">
<span class="icon iconfont icon-churuku"></span>
<div class="name">
出入库
</div>
<div class="code-name">.icon-churuku
</div>
</li>
</ul>
<div class="article markdown">
<h2 id="font-class-">font-class 引用</h2>
<hr>
<p>font-class 是 Unicode 使用方式的一种变种,主要是解决 Unicode 书写不直观,语意不明确的问题。</p>
<p>与 Unicode 使用方式相比,具有如下特点:</p>
<ul>
<li>相比于 Unicode 语意明确,书写更直观。可以很容易分辨这个 icon 是什么。</li>
<li>因为使用 class 来定义图标,所以当要替换图标时,只需要修改 class 里面的 Unicode 引用。</li>
</ul>
<p>使用步骤如下:</p>
<h3 id="-fontclass-">第一步:引入项目下面生成的 fontclass 代码:</h3>
<pre><code class="language-html">&lt;link rel="stylesheet" href="./iconfont.css"&gt;
</code></pre>
<h3 id="-">第二步:挑选相应图标并获取类名,应用于页面:</h3>
<pre><code class="language-html">&lt;span class="iconfont icon-xxx"&gt;&lt;/span&gt;
</code></pre>
<blockquote>
<p>"
iconfont" 是你项目下的 font-family。可以通过编辑项目查看默认是 "iconfont"。</p>
</blockquote>
</div>
</div>
<div class="content symbol">
<ul class="icon_lists dib-box">
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#icon-cc"></use>
</svg>
<div class="name">组合</div>
<div class="code-name">#icon-cc</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#icon-home"></use>
</svg>
<div class="name">首页</div>
<div class="code-name">#icon-home</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#icon-list"></use>
</svg>
<div class="name">商品列表</div>
<div class="code-name">#icon-list</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#icon-daochu"></use>
</svg>
<div class="name">导出</div>
<div class="code-name">#icon-daochu</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#icon-daoru"></use>
</svg>
<div class="name">导入</div>
<div class="code-name">#icon-daoru</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#icon-brand"></use>
</svg>
<div class="name">品牌</div>
<div class="code-name">#icon-brand</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#icon-caigou"></use>
</svg>
<div class="name">采购单</div>
<div class="code-name">#icon-caigou</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#icon-cangku"></use>
</svg>
<div class="name">仓库</div>
<div class="code-name">#icon-cangku</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#icon-supplier"></use>
</svg>
<div class="name">供应商</div>
<div class="code-name">#icon-supplier</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#icon-sku"></use>
</svg>
<div class="name">商品规格</div>
<div class="code-name">#icon-sku</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#icon-churuku"></use>
</svg>
<div class="name">出入库</div>
<div class="code-name">#icon-churuku</div>
</li>
</ul>
<div class="article markdown">
<h2 id="symbol-">Symbol 引用</h2>
<hr>
<p>这是一种全新的使用方式,应该说这才是未来的主流,也是平台目前推荐的用法。相关介绍可以参考这篇<a href="">文章</a>
这种用法其实是做了一个 SVG 的集合,与另外两种相比具有如下特点:</p>
<ul>
<li>支持多色图标了,不再受单色限制。</li>
<li>通过一些技巧,支持像字体那样,通过 <code>font-size</code>, <code>color</code> 来调整样式。</li>
<li>兼容性较差,支持 IE9+,及现代浏览器。</li>
<li>浏览器渲染 SVG 的性能一般,还不如 png。</li>
</ul>
<p>使用步骤如下:</p>
<h3 id="-symbol-">第一步:引入项目下面生成的 symbol 代码:</h3>
<pre><code class="language-html">&lt;script src="./iconfont.js"&gt;&lt;/script&gt;
</code></pre>
<h3 id="-css-">第二步:加入通用 CSS 代码(引入一次就行):</h3>
<pre><code class="language-html">&lt;style&gt;
.icon {
width: 1em;
height: 1em;
vertical-align: -0.15em;
fill: currentColor;
overflow: hidden;
}
&lt;/style&gt;
</code></pre>
<h3 id="-">第三步:挑选相应图标并获取类名,应用于页面:</h3>
<pre><code class="language-html">&lt;svg class="icon" aria-hidden="true"&gt;
&lt;use xlink:href="#icon-xxx"&gt;&lt;/use&gt;
&lt;/svg&gt;
</code></pre>
</div>
</div>
</div>
</div>
<script>
$(document).ready(function () {
$('.tab-container .content:first').show()
$('#tabs li').click(function (e) {
var tabContent = $('.tab-container .content')
var index = $(this).index()
if ($(this).hasClass('active')) {
return
} else {
$('#tabs li').removeClass('active')
$(this).addClass('active')
tabContent.hide().eq(index).fadeIn()
}
})
})
</script>
</body>
</html>

View File

@ -0,0 +1,59 @@
@font-face {
font-family: "iconfont"; /* Project id 4742714 */
src: url('iconfont.woff2?t=1731383577261') format('woff2'),
url('iconfont.woff?t=1731383577261') format('woff'),
url('iconfont.ttf?t=1731383577261') format('truetype');
}
.iconfont {
font-family: "iconfont" !important;
font-size: 16px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.icon-cc:before {
content: "\e679";
}
.icon-home:before {
content: "\e77e";
}
.icon-list:before {
content: "\e685";
}
.icon-daochu:before {
content: "\e854";
}
.icon-daoru:before {
content: "\e664";
}
.icon-brand:before {
content: "\e722";
}
.icon-caigou:before {
content: "\e61d";
}
.icon-cangku:before {
content: "\e668";
}
.icon-supplier:before {
content: "\e611";
}
.icon-sku:before {
content: "\e65b";
}
.icon-churuku:before {
content: "\e600";
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,86 @@
{
"id": "4742714",
"name": "erp自研",
"font_family": "iconfont",
"css_prefix_text": "icon-",
"description": "",
"glyphs": [
{
"icon_id": "836390",
"name": "组合",
"font_class": "cc",
"unicode": "e679",
"unicode_decimal": 59001
},
{
"icon_id": "28892148",
"name": "首页",
"font_class": "home",
"unicode": "e77e",
"unicode_decimal": 59262
},
{
"icon_id": "8478985",
"name": "商品列表",
"font_class": "list",
"unicode": "e685",
"unicode_decimal": 59013
},
{
"icon_id": "16398959",
"name": "导出",
"font_class": "daochu",
"unicode": "e854",
"unicode_decimal": 59476
},
{
"icon_id": "17882700",
"name": "导入",
"font_class": "daoru",
"unicode": "e664",
"unicode_decimal": 58980
},
{
"icon_id": "680982",
"name": "品牌",
"font_class": "brand",
"unicode": "e722",
"unicode_decimal": 59170
},
{
"icon_id": "1614433",
"name": "采购单",
"font_class": "caigou",
"unicode": "e61d",
"unicode_decimal": 58909
},
{
"icon_id": "1680702",
"name": "仓库",
"font_class": "cangku",
"unicode": "e668",
"unicode_decimal": 58984
},
{
"icon_id": "1963493",
"name": "供应商",
"font_class": "supplier",
"unicode": "e611",
"unicode_decimal": 58897
},
{
"icon_id": "10311371",
"name": "商品规格",
"font_class": "sku",
"unicode": "e65b",
"unicode_decimal": 58971
},
{
"icon_id": "25073515",
"name": "出入库",
"font_class": "churuku",
"unicode": "e600",
"unicode_decimal": 58880
}
]
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
src/assets/imgs/404.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

19
src/assets/main.scss Normal file
View File

@ -0,0 +1,19 @@
@forward 'element-plus/theme-chalk/src/common/var.scss' with (
$colors: (
'primary': (
'base': #3c8dbc,
),
),
$table: (
'header-text-color': #333,
'text-color': #333,
),
$select: (
'disabled-color': #666,
),
$disabled: (
'text-color': #666,
)
);
@use 'element-plus/theme-chalk/src/index.scss' as *;

View File

@ -0,0 +1,35 @@
<template>
<el-breadcrumb :separator="props.separator">
<el-breadcrumb-item
v-for="route of infoList"
:key="route.path"
:to="{ path: route.path }"
>{{ route.meta.title }}</el-breadcrumb-item
>
</el-breadcrumb>
</template>
<script setup>
import { useRoute } from 'vue-router'
import { watch, reactive, defineProps } from 'vue'
const route = useRoute()
const infoList = reactive([])
const props = defineProps({
separator: {
type: String,
default: '/'
}
})
watch(
route,
() => {
infoList.splice(0, infoList.length)
route.matched.forEach((value) => {
if (value.meta && value.meta.title) {
infoList.push(value)
}
})
},
{ immediate: true }
)
</script>
<style lang="" scoped></style>

254
src/components/common.js Normal file
View File

@ -0,0 +1,254 @@
import { ElMessage } from 'element-plus'
// 时间戳转日期
export function formatDate(now) {
var date = new Date(now)
var Y = date.getFullYear() + '-'
var M = (date.getMonth()+1 < 10 ? '0'+(date.getMonth() + 1) : date.getMonth() + 1) + '-'
var D = (date.getDate() < 10 ? '0'+(date.getDate()) : date.getDate()) + ' '
var h = (date.getHours() < 10 ? "0" + date.getHours() : date.getHours()) + ':'
var m = (date.getMinutes() < 10 ? "0" + date.getMinutes() : date.getMinutes()) + ':'
var s = date.getSeconds() < 10 ? "0" + date.getSeconds() : date.getSeconds()
return Y + M + D + h + m + s
}
export function dateTimeStr(str = "y-m-d h:i:s", day){
var date = new Date(day),
year = date.getFullYear(), //年
month = date.getMonth() + 1, //月
day = date.getDate(), //日
hour = date.getHours(), //时
minute = date.getMinutes() < 10 ? "0" + date.getMinutes() : date.getMinutes(), //分
second = date.getSeconds() < 10 ? "0" + date.getSeconds() : date.getSeconds(); //秒
month >= 1 && month <= 9 ? (month = "0" + month) : "";
day >= 0 && day <= 9 ? (day = "0" + day) : "";
hour >= 0 && hour <= 9 ? (hour = "0" + hour) : "";
if(str.indexOf('y') != -1){
str = str.replace('y', year)
}
if(str.indexOf('m') != -1){
str = str.replace('m', month)
}
if(str.indexOf('d') != -1){
str = str.replace('d', day)
}
if(str.indexOf('h') != -1){
str = str.replace('h', hour)
}
if(str.indexOf('i') != -1){
str = str.replace('i', minute)
}
if(str.indexOf('s') != -1){
str = str.replace('s', second)
}
return str;
}
// 比较两个时间大小
export function compareDate(date1, date2) {
let str1 = new Date(date1.replace(/-/g, "/")).getTime()
let str2 = new Date(date2.replace(/-/g, "/")).getTime()
return str2 - str1 >= 0
}
// 获取当前日期前几天的日期
export function getOldDay(day) {
var dd = new Date();
dd.setDate(dd.getDate()-day); //获取AddDayCount天后的日期
return formatDate(new Date(dd))
}
// 循环数组的笛卡尔积
export function cartesianProductOf () {
return Array.prototype.reduce.call(arguments, (a, b) => {
const ret = []
a.forEach(function (a) {
b.forEach(function (b) {
ret.push(a.concat([b]))
})
})
return ret
}, [[]])
}
/**
* @description 复制
* @param {*} id DOM ID
*/
export function copyDomText (id) {
const node = document.getElementById(id)
if (node) {
let createRange = document.createRange()
createRange.selectNodeContents(document.getElementById(id))
const selection = document.getSelection()
selection.removeAllRanges()
selection.addRange(createRange)
document.execCommand('Copy')
selection.removeAllRanges()
alert('已复制')
}
}
export function arrayOperations(arr, newIndex, oldIndex) {
arr[newIndex] = arr.splice(oldIndex, 1, arr[newIndex])[0]
return arr
}
export function variableType(val) {
const valEnum = {
'[object String]': 'String',
'[object Number]': 'Number',
'[object Array]': 'Array',
'[object Object]': 'Object',
'[object Null]': 'Null',
'[object Undefined]': 'Undefined',
'[object Boolean]': 'Boolean',
'[object Function]': 'Function',
}
return valEnum[Object.prototype.toString.call(val)]
}
export function clearForm(data) {
console.log('data========', data)
if (variableType(data) !== 'Object') {
console.log('data必须要是对象')
return
}
Object.keys(data).forEach((item) => {
if (variableType(data[item]) === 'String') {
data[item] = ''
}
if (variableType(data[item]) === 'Null') {
data[item] = null
}
if (variableType(data[item]) === 'Boolean') {
data[item] = false
}
if (variableType(data[item]) === 'Undefined') {
data[item] = undefined
}
if (variableType(data[item]) === 'Number') {
data[item] = 0
}
if (variableType(data[item]) === 'Object') {
console.log('是对象')
clearForm(data[item])
}
if (variableType(data[item]) === 'Array') {
data[item] = []
}
return item
})
return data
}
/**
* @description: 时间段转时间戳
* @param {*} t
* @return {*}
*/
export function getTargetTime(t) {
var d = t.split(' ')[0],
h = t.split(' ')[1],
date = new Date()
date.setYear(d.split('-')[0])
date.setMonth(d.split('-')[1] - 1)
date.setDate(d.split('-')[2])
date.setHours(h.split(':')[0])
date.setMinutes(h.split(':')[1])
date.setSeconds(h.split(':')[2])
return date.getTime()
}
// 时间戳转时间段
export function formatDate1(item) {
var date = new Date(item)
var YY = date.getFullYear() + '-'
var MM = (date.getMonth() + 1 < 10 ? '0' + (date.getMonth() + 1) : date.getMonth() + 1) + '-'
var DD = date.getDate() < 10 ? '0' + date.getDate() : date.getDate()
var hh = (date.getHours() < 10 ? '0' + date.getHours() : date.getHours()) + ':'
var mm = (date.getMinutes() < 10 ? '0' + date.getMinutes() : date.getMinutes()) + ':'
var ss = date.getSeconds() < 10 ? '0' + date.getSeconds() : date.getSeconds()
return YY + MM + DD + ' ' + hh + mm + ss
}
// 复制内容
export function copyContent(value) {
var input = document.createElement('input')
// 把文字放进input中供复制
input.value = value
document.body.appendChild(input)
// 选中创建的input
input.select()
var copy_result = document.execCommand('copy')
if (copy_result) {
ElMessage({ type: 'success', message: '复制成功' })
} else {
ElMessage({ type: 'error', message: '复制失败' })
}
document.body.removeChild(input)
}
// 判断标题是否有特殊字符(表情)
export function isEmojiCharacter(substring){
for (var i = 0; i < substring.length; i ++){
var hs = substring.charCodeAt(i);
if (0xd800 <= hs && hs <= 0xdbff){
if (substring.length> 1){
var ls = substring.charCodeAt(i + 1);
var uc =((hs - 0xd800)* 0x400)+(ls - 0xdc00)+ 0x10000;
if (0x1d000 <= uc && uc <= 0x1f77f){
return true;
}
}
} else if (substring.length> 1){
var ls = substring.charCodeAt(i + 1);
if (ls == 0x20e3){
return true ;
}
} else {
if (0x2100 <= hs && hs <= 0x27ff){
return true ;
} else if (0x2B05 <= hs && hs <= 0x2b07){
return true ;
} else if (0x2934 <= hs && hs <= 0x2935){
return true ;
} else if (0x3297 <= hs && hs <= 0x3299){
return true ;
} else if (hs == 0xa9 || hs == 0xae || hs == 0x303d || hs == 0x3030
|| hs == 0x2b55 || hs == 0x2b1c || hs == 0x2b1b
|| hs == 0x2b50){
return true ;
}
}
}
}
export function dateFormatTxt(time) {
const delta = (new Date().getTime() - new Date(time).getTime()) / 1000
if (delta / (60 * 60 * 24 * 365) > 1)
return `${parseInt(delta / (60 * 60 * 24 * 365))}年前`
if (delta / (60 * 60 * 24 * 30) > 1)
return `${parseInt(delta / (60 * 60 * 24 * 30))}个月前`
if (delta / (60 * 60 * 24 * 7) > 1)
return `${parseInt(delta / (60 * 60 * 24 * 7))}周前`
if (delta / (60 * 60 * 24) > 1)
return `${parseInt(delta / (60 * 60 * 24))}天前`
if (delta / (60 * 60) > 1) return `${parseInt(delta / (60 * 60))}小时前`
if (delta / 60 > 1) return `${parseInt(delta / 60)}分钟前`
return '刚刚'
}
export function parseErrors(errors) {
if(errors && Object.keys(errors).length !== 0) {
for (let key in errors) {
if (errors.hasOwnProperty(key)) {
errors[key].forEach((it) => {
ElMessage({ message: it, type: 'info', customClass: 'vanMsg' })
})
}
}
}
}

18
src/main.js Normal file
View File

@ -0,0 +1,18 @@
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import zhCn from 'element-plus/es/locale/lang/zh-cn'
import '@/assets/icon/iconfont.css'
import '@/assets/main.scss'
createApp(App)
.use(store)
.use(router)
.use(ElementPlus, { locale: zhCn, zIndex: 3000, size: 'default' })
.mount('#app')
document.title = 'ERP-管理系统';
document.execCommand("BackgroundImageCache", false, true);

89
src/router/index.js Normal file
View File

@ -0,0 +1,89 @@
import { createRouter, createWebHashHistory } from 'vue-router'
import Layout from 'views/layout/index.vue'
import { constantRouterComponents } from './white'
export const routes = [
{
path: '/',
redirect: '/login',
hidden: true,
component: () => import('views/login/index.vue'),
},
{
path: '/login',
name: 'login',
hidden: true,
component: () => import('views/login/index.vue')
},
{
path: '/404',
name: '404',
hidden: true,
component: () => import('views/login/404.vue')
},
{
path: '/layout',
name: 'Layout',
component: Layout,
children: [
{
path: '/home',
name: 'home',
title: '首页',
component: () => import('views/home/index.vue')
}
]
}
]
export function getMenusRoute() {
let list = []
let menus = JSON.parse(localStorage.getItem('menus'))
menus && menus.forEach((item) => {
if(item.path != '/home') {
let obj = {
path: item.path,
name: item.name,
full_path: item.full_path,
component: (constantRouterComponents[item.name]) || (() => import(`${item.full_path}`))
}
list.push(obj)
}
})
return list
}
const router = createRouter({
history: createWebHashHistory(),
routes
})
let registerRouteFresh = true
router.beforeEach((to, from, next) => {
if (to.path !== '/login' && to.path !== '/chooseUser' && to.path !== '/homeLogin' && to.path !== '/backstage') {
if (localStorage.getItem('token')) {
let list = getMenusRoute()
if (registerRouteFresh && list.length) {
list.forEach((itemRouter) => {
router.addRoute('Layout', itemRouter)
})
router.addRoute({
path: '/:pathMatch(.*)',
redirect: '/404'
})
next(to)
registerRouteFresh = false
} else {
next()
}
} else {
next('/login')
}
} else {
next()
}
})
export default router

96
src/router/white.js Normal file
View File

@ -0,0 +1,96 @@
export const whiteList = [
{
path: '/commodity/list',
name: 'commodityList',
full_path: 'views/commodity/list.vue',
component: () => import('views/commodity/list')
},
{
path: '/commodity/all',
name: 'allgoods',
full_path: 'views/commodity/all.vue',
component: () => import('views/commodity/all')
},
{
path: '/commodity/sku',
name: 'commoditySku',
full_path: 'views/commodity/sku.vue',
component: () => import('views/commodity/sku')
},
{
path: '/commodity/combination',
name: 'combination',
full_path: 'views/commodity/combination.vue',
component: () => import('views/commodity/combination')
},
{
path: '/commodity/brand',
name: 'commodityBrand',
full_path: 'views/commodity/brand.vue',
component: () => import('views/commodity/brand')
},
{
path: '/purchase/index',
name: 'purchase',
full_path: 'views/purchase/index.vue',
component: () => import('views/purchase/index')
},
{
path: '/warehouse/index',
name: 'warehouse',
full_path: 'views/warehouse/index.vue',
component: () => import('views/warehouse/index')
},
{
path: '/warehouse/io',
name: 'warehouseIO',
full_path: 'views/warehouse/io.vue',
component: () => import('views/warehouse/io')
},
{
path: '/supplier/index',
name: 'supplier',
full_path: 'views/supplier/index.vue',
component: () => import('views/supplier/index')
},
{
path: '/statistics/index',
name: 'statistics',
full_path: 'views/statistics/index.vue',
component: () => import('views/statistics/index')
},
{
path: '/permission/role',
name: 'role',
full_path: 'views/permission/role.vue',
component: () => import('views/permission/role')
},
{
path: '/permission/menus',
name: 'menus',
full_path: 'views/permission/menus.vue',
component: () => import('views/permission/menus')
},
{
path: '/shops/index',
name: 'shops',
full_path: 'views/shops/index.vue',
component: () => import('views/shops/index')
}
]
export const constantRouterComponents = {
'commodityList': () => import('views/commodity/list'),
'allgoods': () => import('views/commodity/all'),
'commoditySku': () => import('views/commodity/sku'),
'combination': () => import('views/commodity/combination'),
'commodityBrand': () => import('views/commodity/brand'),
'purchase': () => import('views/purchase/index'),
'warehouse': () => import('views/warehouse/index'),
'warehouseIO': () => import('views/warehouse/io'),
'supplier': () => import('views/supplier/index'),
'statistics': () => import('views/statistics/index'),
'role': () => import('views/permission/role'),
'menus': () => import('views/permission/menus'),
'shops': () => import('views/shops/index')
}

82
src/store/index.js Normal file
View File

@ -0,0 +1,82 @@
import { createStore } from 'vuex'
export default createStore({
state: {
msgNum: 0,
orderNum: 0,
noticeNum: 0,
workNum: 0,
tousuNum: 0,
annNum: 0,
orderType: '',
orderId: '',
workId: '',
msgDialog: false
},
mutations: {
SET_MSGNUM: (state, num) => {
state.msgNum = num
},
SET_ORDERNUM: (state, num) => {
state.orderNum = num
},
SET_NOTICENUM: (state, num) => {
state.noticeNum = num
},
SET_WORKNUM: (state, num) => {
state.workNum = num
},
SET_TOUSUNUM: (state, num) => {
state.tousuNum = num
},
SET_ORDERTYPE: (state, val) => {
state.orderType = val
},
SET_ORDERID: (state, val) => {
state.orderId = val
},
SET_WORKID: (state, val) => {
state.workId = val
},
SET_MSGDIALOG: (state, flag) => {
state.msgDialog = flag
},
SET_ANNNUM: (state, num) => {
state.annNum = num
},
},
actions: {
setMsgNum({ commit }, num) {
commit('SET_MSGNUM', num)
},
setOrderNum({ commit }, num) {
commit('SET_ORDERNUM', num)
},
setNoticeNum({ commit }, num) {
commit('SET_NOTICENUM', num)
},
setWorkNum({ commit }, num) {
commit('SET_WORKNUM', num)
},
setTousuNum({ commit }, num) {
commit('SET_TOUSUNUM', num)
},
setOrderType({ commit }, val) {
commit('SET_ORDERTYPE', val)
},
setOrderId({ commit }, val) {
commit('SET_ORDERID', val)
},
setWorkId({ commit }, val) {
commit('SET_WORKID', val)
},
setMsgDialog({ commit }, flag) {
commit('SET_MSGDIALOG', flag)
},
setAnnNum({ commit }, num) {
commit('SET_ANNNUM', num)
}
},
modules: {
}
})

220
src/views/commodity/all.vue Normal file
View File

@ -0,0 +1,220 @@
<template>
<div class="pageBox">
<div class="searchBox">
<div class="row row1">
<span class="span">商品名称</span>
<div class="right"><el-input v-model="filter.keyword" class="wid100" clearable></el-input></div>
</div>
<div class="row row1">
<span class="span">商品编码</span>
<div class="right"><el-input v-model="filter.goods_code" class="wid100" clearable></el-input></div>
</div>
<div class="row row1">
<span class="span">规格编码</span>
<div class="right"><el-input v-model="filter.sku_code" class="wid100" clearable></el-input></div>
</div>
<div class="row row1">
<span class="span">是否单品</span>
<div class="right">
<el-select v-model="filter.single" placeholder="请选择" clearable class="wid100">
<el-option label="组合商品" :value="0" />
<el-option label="单品" :value="1" />
</el-select>
</div>
</div>
<div class="row row1">
<span class="span">是否售卖</span>
<div class="right">
<el-select v-model="filter.on_sale" placeholder="请选择" clearable class="wid100">
<el-option label="是" :value="1" />
<el-option label="否" :value="0" />
</el-select>
</div>
</div>
<div class="row">
<el-button type="primary" @click="handleSearch"><el-icon><Search /></el-icon>&nbsp;筛选</el-button>
</div>
</div>
<el-card shadow="never">
<el-table :data="goodsList" style="width: 100%" border v-loading="loading">
<el-table-column prop="id" label="ID" align="center" width="80" />
<el-table-column prop="type" label="商品信息">
<template #default="scope">
<div>{{ scope.row.goods_name }}</div>
<span style="color: #666;">{{ scope.row.goods_code }}</span>
</template>
</el-table-column>
<!-- <el-table-column prop="goods_stock" label="商品库存" align="center" /> -->
<el-table-column prop="type" label="规格信息">
<template #default="scope">
<div>{{ scope.row.sku_name }}</div>
<span style="color: #666;">{{ scope.row.sku_code }}</span>
</template>
</el-table-column>
<el-table-column prop="sku_number" label="规格数量" align="center" />
<el-table-column prop="sku_stock" label="规格库存" align="center" />
<el-table-column prop="type" label="是否单品" align="center">
<template #default="scope">
<el-tag type="primary" v-if="scope.row.single === 0">组合商品</el-tag>
<el-tag type="danger" v-else-if="scope.row.single === 1">单品</el-tag>
</template>
</el-table-column>
<el-table-column label="是否售卖" align="center">
<template #default="scope">
<el-switch
v-model="scope.row.on_sale"
:active-value="1"
:inactive-value="0"
active-text="是"
inactive-text="否"
inline-prompt
:loading="scope.row.loading"
:before-change="()=>beforeChange($event, scope.row)" />
</template>
</el-table-column>
<!-- <el-table-column prop="created_at" label="创建时间" align="center" /> -->
</el-table>
<div class="page-pagination">
<el-pagination
:current-page="page"
background
layout="prev, pager, next, sizes, total"
:total="total"
:page-sizes="[10, 50, 100, 500, 1000]"
:page-size="pageSize"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"></el-pagination>
</div>
</el-card>
</div>
</template>
<script>
import { onMounted, reactive, toRefs } from "vue"
import { get, post } from "@/api/request"
import { Search } from '@element-plus/icons'
import { ElMessage, ElMessageBox } from 'element-plus'
export default {
components: {
Search
},
setup() {
const data = reactive({
filter: {},
goodsList: [],
page: 1,
pageSize: 10,
total: 0,
loading: false
})
function handleSearch() {
data.page = 1
fetchData()
}
const fetchData = () => {
data.loading = true
let params = {
...data.filter,
page: data.page,
pageSize: data.pageSize
}
get(`/api/mxErpGoods`, params).then((res) => {
data.goodsList = res.data
data.total = res.meta.total
data.loading = false
}).catch(() => {
data.loading = false
})
}
function handleCurrentChange(e) {
data.page = e
fetchData()
}
function handleSizeChange(e) {
data.page = 1
data.pageSize = e
fetchData()
}
const beforeChange = (e, row) => {
let on_sale = row.on_sale
row.loading = true
return new Promise((resolve, reject) => {
ElMessageBox.confirm('确定修改该状态吗?', '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
post(`/api/mxErpGoods/${row.id}`, {on_sale: on_sale == 1 ? 0 : 1}, 'PUT').then(() => {
ElMessage({ type: 'success', message: '修改成功' })
row.loading = false
return resolve(true)
}).catch(() => {
row.loading = false
return reject(new Error('Error'))
})
}).catch(() => {
row.loading = false
return reject(new Error('Error'))
})
})
}
onMounted(() => {
fetchData()
})
return {
...toRefs(data),
handleSearch,
handleCurrentChange,
handleSizeChange,
fetchData,
beforeChange
}
}
}
</script>
<style lang="scss" scoped>
.searchBox{
display: flex;
flex-wrap: wrap;
background-color: #fff;
padding: 15px 0 0 0;
border-radius: 4px;
margin-bottom: 15px;
.row{
display: flex;
align-items: center;
width: auto;
box-sizing: border-box;
margin-bottom: 15px;
margin-right: 15px;
&.row1{
width: 300px;
}
.span{
display: block;
width: 80px;
font-size: 14px;
text-align: right;
box-sizing: border-box;
}
.right{
width: calc(100% - 80px);
}
}
}
</style>

View File

@ -0,0 +1,275 @@
<template>
<div class="pageBox">
<!-- <div class="searchBox">
<div class="row">
<span class="span">商品名称</span>
<div class="right"><el-input v-model="goods_name" class="wid100" clearable></el-input></div>
</div>
<div class="row">
<el-button type="primary" @click="handleSearch"><el-icon><Search /></el-icon>&nbsp;筛选</el-button>
</div>
</div> -->
<el-card shadow="never">
<div class="opaBox">
<el-button type="primary" @click="handleAdd"><el-icon><Plus /></el-icon>&nbsp;新增</el-button>
<!-- <el-button type="warning" @click="handleExport"><span class="iconfont icon-daochu"></span>&nbsp;导出</el-button> -->
</div>
<el-table :data="goodsList" style="width: 100%" border v-loading="loading">
<el-table-column prop="id" label="ID" width="80" align="center" />
<el-table-column prop="name" label="品牌名称" align="center" />
<el-table-column prop="type" label="品牌类型" align="center">
<template #default="scope">
<span>{{ brandType[scope.row.type] }}</span>
</template>
</el-table-column>
<el-table-column label="品牌状态" align="center">
<template #default="scope">
<span>{{ scope.row.status ? '启用' : '不启用' }}</span>
</template>
</el-table-column>
<el-table-column prop="admin_user.username" label="添加人" align="center" />
<el-table-column prop="note" label="备注" align="center" />
<el-table-column prop="sort" label="排序" align="center" />
<el-table-column label="操作" align="center" width="150">
<template #default="scope">
<el-button type="primary" circle @click="handleEdit(scope.row)"><el-icon><Edit /></el-icon></el-button>
</template>
</el-table-column>
</el-table>
<div class="page-pagination">
<el-pagination
background
:current-page="page"
layout="prev, pager, next, sizes, total"
:total="total"
:page-sizes="[10, 50, 100]"
:page-size="pageSize"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"></el-pagination>
</div>
</el-card>
<el-dialog v-model="showDialog" width="900px" :title="opaType == 'add' ? '新增' : '编辑' ">
<el-form label-position="right" label-width="110px">
<el-form-item label="品牌名称:">
<el-input v-model="itemInfo.name" clearable></el-input>
</el-form-item>
<el-form-item label="品牌类型:">
<el-radio-group v-model="itemInfo.type">
<el-radio label="self">自有</el-radio>
<el-radio label="custom">定制</el-radio>
<el-radio label="default">默认</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="品牌状态:">
<el-radio-group v-model="itemInfo.status">
<el-radio :label="1">启用</el-radio>
<el-radio :label="0">不启用</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="备注:">
<el-input v-model="itemInfo.note" type="textarea" :rows="4"></el-input>
</el-form-item>
<el-form-item label="排序:">
<el-input-number v-model="itemInfo.sort" :min="0" />
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="showDialog = false">取消</el-button>
<el-button type="primary" @click="commitOpa()" :loading="opa_loading">确定</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script>
import { onMounted, reactive, toRefs } from "vue"
import { get, post } from "@/api/request"
import { Search, Plus, Edit, ZoomIn, Delete } from '@element-plus/icons'
import { ElMessage } from 'element-plus'
import { parseErrors } from 'components/common'
export default {
components: {
Search, Plus, Edit, ZoomIn, Delete
},
setup() {
const data = reactive({
goodsList: [],
page: 1,
pageSize: 10,
total: 0,
loading: false,
pickerTime: [],
opaType: '',
showDialog: false,
opa_loading: false,
brandType: {
'self': '自有',
'custom': '定制',
'default': '默认'
},
itemInfo: {}
})
function handleSearch() {
data.page = 1
fetchData()
}
const fetchData = () => {
data.loading = true
let params = {
page: data.page,
pageSize: data.pageSize,
service_id: data.service_id,
start_date: data.pickerTime ? data.pickerTime[0] : '',
end_date: data.pickerTime ? data.pickerTime[1] : ''
}
get(`/api/brands`, params).then((res) => {
data.goodsList = res.data
data.total = res.meta.total
data.loading = false
}).catch(() => {
data.loading = false
})
}
function handleCurrentChange(e) {
data.page = e
fetchData()
}
function handleSizeChange(e) {
data.page = 1
data.pageSize = e
fetchData()
}
function handleAdd() {
data.opaType = 'add'
data.itemInfo = {
status: 1,
sort: 0
}
data.showDialog = true
}
function handleEdit(item) {
data.opaType = 'edit'
data.itemInfo = JSON.parse(JSON.stringify(item))
data.showDialog = true
}
function commitOpa() {
data.opa_loading = true
let params = {
...data.itemInfo
}
if(data.opaType == 'add') {
post(`/api/brands`, params).then(() => {
data.page = 1
fetchData()
ElMessage({ type: 'success', message: '新增成功' })
data.showDialog = false
data.opa_loading = false
}).catch((err) => {
data.opa_loading = false
parseErrors(err.response.data.errors)
})
} else {
post(`/api/brands/${data.itemInfo.id}`, params, 'PUT').then(() => {
fetchData()
ElMessage({ type: 'success', message: '编辑成功' })
data.showDialog = false
data.opa_loading = false
}).catch((err) => {
data.opa_loading = false
parseErrors(err.response.data.errors)
})
}
}
onMounted(() => {
fetchData()
})
return {
...toRefs(data),
handleSearch,
handleCurrentChange,
handleSizeChange,
fetchData,
handleAdd,
handleEdit,
commitOpa,
}
}
}
</script>
<style lang="scss" scoped>
.searchBox{
display: flex;
flex-wrap: wrap;
background-color: #fff;
padding: 15px 0 0 0;
border-radius: 4px;
margin-bottom: 15px;
.row{
display: flex;
align-items: center;
width: 20%;
box-sizing: border-box;
margin-bottom: 15px;
&.row1{
width: 25%;
}
.span{
display: block;
width: 80px;
font-size: 14px;
text-align: right;
box-sizing: border-box;
}
.right{
width: calc(100% - 100px);
}
.wid100{
width: 100%;
}
}
}
.opaBox{
margin-bottom: 15px;
}
.imgBox{
display: flex;
flex-wrap: wrap;
.el-image{
width: 60px;
height: 60px;
border-radius: 4px;
margin-right: 10px;
}
}
.skuBox{
border: 1px solid #e5e5e5;
border-radius: 5px;
padding: 15px 0;
margin-bottom: 15px;
background-color: #f3f3f3;
.tit{
padding-left: 40px;
font-weight: 600;
font-size: 15px;
margin-bottom: 15px;
}
}
</style>

View File

@ -0,0 +1,339 @@
<template>
<div class="pageBox">
<div class="searchBox">
<div class="row">
<span class="span">商品名称</span>
<div class="right"><el-input v-model="filter.title" class="wid100" clearable></el-input></div>
</div>
<div class="row">
<span class="span">组合编码</span>
<div class="right"><el-input v-model="filter.combination_goods_code" class="wid100" clearable></el-input></div>
</div>
<div class="row">
<span class="span">状态</span>
<div class="right">
<el-select v-model="filter.status" placeholder="请选择" clearable class="wid100">
<el-option label="启用" :value="1" />
<el-option label="不启用" :value="0" />
</el-select>
</div>
</div>
<div class="row">
<span class="span"></span>
<div class="right"><el-button type="primary" @click="handleSearch"><el-icon><Search /></el-icon>&nbsp;筛选</el-button></div>
</div>
</div>
<el-card shadow="never">
<div class="opaBox">
<el-button type="primary" @click="handleAdd"><el-icon><Plus /></el-icon>&nbsp;新增</el-button>
<!-- <el-button type="warning" @click="handleExport"><span class="iconfont icon-daochu"></span>&nbsp;导出</el-button> -->
</div>
<el-table :data="goodsList" style="width: 100%" border v-loading="loading">
<el-table-column prop="id" label="ID" width="80" align="center" />
<el-table-column prop="title" label="组合商品名称" align="center" />
<el-table-column prop="combination_goods_code" label="组合编码" align="center" />
<el-table-column prop="actual_inventory" label="实际库存" align="center" />
<el-table-column prop="lock_in_stock" label="锁定库存" align="center" />
<el-table-column prop="available_inventory" label="可售库存" align="center" />
<el-table-column label="状态" align="center">
<template #default="scope">
<span>{{ scope.row.status ? '启用' : '不启用' }}</span>
</template>
</el-table-column>
<el-table-column prop="admin_user.username" label="添加人" align="center" />
<el-table-column prop="created_at" label="添加时间" align="center" />
<el-table-column label="操作" align="center" width="150">
<template #default="scope">
<el-button type="primary" circle @click="handleEdit(scope.row)"><el-icon><Edit /></el-icon></el-button>
</template>
</el-table-column>
</el-table>
<div class="page-pagination">
<el-pagination
background
:current-page="page"
layout="prev, pager, next, sizes, total"
:total="total"
:page-sizes="[10, 50, 100]"
:page-size="pageSize"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"></el-pagination>
</div>
</el-card>
<el-dialog v-model="showDialog" width="900px" :title="opaType == 'add' ? '新增' : '编辑' ">
<el-form label-position="right" label-width="110px">
<el-form-item label="组合商品名称:">
<el-input v-model="itemInfo.title" clearable></el-input>
</el-form-item>
<el-form-item label="组合编码:">
<el-input v-model="itemInfo.combination_goods_code" clearable></el-input>
</el-form-item>
<el-form-item label="锁定库存:">
<el-input v-model="itemInfo.lock_in_stock" clearable></el-input>
</el-form-item>
<el-form-item label="状态:">
<el-radio-group v-model="itemInfo.status">
<el-radio :label="1">启用</el-radio>
<el-radio :label="0">不启用</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="备注:">
<el-input v-model="itemInfo.note" type="textarea" :rows="4"></el-input>
</el-form-item>
<el-form-item label="排序:">
<el-input-number v-model="itemInfo.sort" :min="0" />
</el-form-item>
<el-form-item label=" ">
<b style="margin-top: 20px;">参与组合商品</b>
</el-form-item>
</el-form>
<div v-for="(item, i) in goodsSkus" :key="i" class="skuBox">
<el-form label-width="110px" :inline="true">
<el-form-item label="规格:">
<el-select v-model="item.sku_id" placeholder="请选择" clearable filterable style="width: 160px;">
<el-option v-for="it in skusList" :key="it.id" :label="it.goods && it.goods.title + '' + it.title + ''" :value="it.id" />
</el-select>
</el-form-item>
<el-form-item label="数量:">
<el-input-number v-model="item.num" :min="0" />
</el-form-item>
<el-form-item label=" ">
<el-button type="danger" @click="handleSkuDelete(i)" size="small"><el-icon><Delete /></el-icon>&nbsp;删除</el-button>
</el-form-item>
</el-form>
</div>
<el-form label-position="right" label-width="110px">
<el-form-item label="">
<el-button type="success" @click="toAddSku()">新增</el-button>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="showDialog = false">取消</el-button>
<el-button type="primary" @click="commitOpa()" :loading="opa_loading">确定</el-button>
</span>
</template>
</el-dialog>
<!-- 预览图片 -->
<el-dialog v-model="picVisible" center width="800px">
<img :src="dialogImageUrl" style="max-width: 700px; margin: 0 auto;display: block;" />
</el-dialog>
</div>
</template>
<script>
import { onMounted, reactive, toRefs } from "vue"
import { get, post } from "@/api/request"
import { Search, Plus, Edit, ZoomIn, Delete } from '@element-plus/icons'
import { ElMessage, ElMessageBox } from 'element-plus'
import { parseErrors } from 'components/common'
export default {
components: {
Search, Plus, Edit, ZoomIn, Delete
},
setup() {
const data = reactive({
filter: {},
goodsList: [],
page: 1,
pageSize: 10,
total: 0,
loading: false,
opaType: '',
showDialog: false,
opa_loading: false,
itemInfo: {},
goodsSkus: [],
skusList: []
})
function handleSearch() {
data.page = 1
fetchData()
}
const fetchData = () => {
data.loading = true
let params = {
page: data.page,
pageSize: data.pageSize,
...data.filter
}
get(`/api/combination-goods-skus`, params).then((res) => {
data.goodsList = res.data
data.total = res.meta.total
data.loading = false
}).catch(() => {
data.loading = false
})
}
function handleCurrentChange(e) {
data.page = e
fetchData()
}
function handleSizeChange(e) {
data.page = 1
data.pageSize = e
fetchData()
}
function handleAdd() {
data.opaType = 'add'
data.fileList = []
data.goodsSkus = []
data.itemInfo = {
sort: 0,
status: 1
}
data.showDialog = true
}
function handleEdit(item) {
data.opaType = 'edit'
get(`/api/combination-goods-skus/${item.id}`).then((res) => {
data.itemInfo = res.data
data.goodsSkus = res.data.combination_goods_sku || []
data.showDialog = true
})
}
function commitOpa() {
data.opa_loading = true
let params = {
...data.itemInfo
}
params.goodsSkus = data.goodsSkus
if(data.opaType == 'add') {
post(`/api/combination-goods-skus`, params).then(() => {
data.page = 1
fetchData()
ElMessage({ type: 'success', message: '新增成功' })
data.showDialog = false
data.opa_loading = false
}).catch((err) => {
data.opa_loading = false
parseErrors(err.response.data.errors)
})
} else {
post(`/api/combination-goods-skus/${data.itemInfo.id}`, params, 'PUT').then(() => {
fetchData()
ElMessage({ type: 'success', message: '编辑成功' })
data.showDialog = false
data.opa_loading = false
}).catch((err) => {
data.opa_loading = false
parseErrors(err.response.data.errors)
})
}
}
function handleSkuDelete(index) {
data.goodsSkus.splice(index, 1)
}
function toAddSku() {
let sku = {
sku_id: '',
num: 1
}
data.goodsSkus.push(sku)
}
function getSkusList() {
get(`/api/all/goods-skus`).then((res) => {
data.skusList = res.data
})
}
onMounted(() => {
fetchData()
getSkusList()
})
return {
...toRefs(data),
handleSearch,
handleCurrentChange,
handleSizeChange,
fetchData,
handleAdd,
handleEdit,
commitOpa,
handleSkuDelete,
toAddSku,
getSkusList
}
}
}
</script>
<style lang="scss" scoped>
.searchBox{
display: flex;
flex-wrap: wrap;
background-color: #fff;
padding: 15px 0 0 0;
border-radius: 4px;
margin-bottom: 15px;
.row{
display: flex;
align-items: center;
width: 20%;
box-sizing: border-box;
margin-bottom: 15px;
&.row1{
width: 25%;
}
.span{
display: block;
width: 80px;
font-size: 14px;
text-align: right;
box-sizing: border-box;
}
.right{
width: calc(100% - 100px);
}
.wid100{
width: 100%;
}
}
}
.opaBox{
margin-bottom: 15px;
}
.imgBox{
display: flex;
flex-wrap: wrap;
.el-image{
width: 60px;
height: 60px;
border-radius: 4px;
margin-right: 10px;
}
}
.skuBox{
border: 1px solid #e5e5e5;
border-radius: 5px;
padding: 15px 0;
margin-bottom: 15px;
background-color: #f3f3f3;
.tit{
padding-left: 40px;
font-weight: 600;
font-size: 15px;
margin-bottom: 15px;
}
}
</style>

View File

@ -0,0 +1,494 @@
<template>
<div class="pageBox">
<div class="searchBox">
<div class="row">
<span class="span">商品名称</span>
<div class="right"><el-input v-model="filter.title" class="wid100" clearable></el-input></div>
</div>
<div class="row">
<span class="span">商品编码</span>
<div class="right"><el-input v-model="filter.goods_code" class="wid100" clearable></el-input></div>
</div>
<div class="row">
<span class="span">商品品牌</span>
<div class="right">
<el-select v-model="filter.brand_id" placeholder="请选择" clearable class="wid100" filterable>
<el-option v-for="it in brandList" :key="it.id" :label="it.name" :value="it.id" />
</el-select>
</div>
</div>
<div class="row">
<span class="span">仓库</span>
<div class="right">
<el-select v-model="filter.warehouse_id" placeholder="请选择" clearable class="wid100" filterable>
<el-option v-for="it in warehouseList" :key="it.id" :label="it.name" :value="it.id" />
</el-select>
</div>
</div>
<div class="row">
<span class="span">商品状态</span>
<div class="right">
<el-select v-model="filter.status" placeholder="请选择" clearable class="wid100">
<el-option label="启用" :value="1" />
<el-option label="不启用" :value="0" />
</el-select>
</div>
</div>
<div class="row">
<span class="span"></span>
<div class="right"><el-button type="primary" @click="handleSearch"><el-icon><Search /></el-icon>&nbsp;筛选</el-button></div>
</div>
</div>
<el-card shadow="never">
<div class="opaBox">
<el-button type="primary" @click="handleAdd"><el-icon><Plus /></el-icon>&nbsp;新增</el-button>
<!-- <el-button type="warning" @click="handleExport"><span class="iconfont icon-daochu"></span>&nbsp;导出</el-button> -->
</div>
<el-table :data="goodsList" style="width: 100%" border v-loading="loading">
<el-table-column prop="id" label="ID" width="80" align="center" />
<el-table-column prop="title" label="商品名称" align="center" />
<el-table-column prop="goods_code" label="商品编码" align="center" />
<el-table-column prop="brand.name" label="商品品牌" align="center" />
<el-table-column label="商品图片" align="center" min-width="150">
<template #default="scope">
<div class="imgBox">
<el-image :z-index="9999" v-for="(item, index) in scope.row.images" :key="index" :src="item" :hide-on-click-modal="true" :preview-src-list="[item]" fit="cover" :preview-teleported="true" />
</div>
</template>
</el-table-column>
<el-table-column prop="warehouse.name" label="仓库" align="center" />
<el-table-column prop="introduce" label="说明" align="center" />
<el-table-column prop="admin_user.username" label="添加人" align="center" />
<el-table-column label="商品状态" align="center">
<template #default="scope">
<span>{{ scope.row.status ? '启用' : '不启用' }}</span>
</template>
</el-table-column>
<el-table-column prop="sort" label="排序" align="center" />
<el-table-column prop="created_at" label="添加时间" align="center" />
<el-table-column label="操作" align="center" width="150">
<template #default="scope">
<el-button size="small" type="primary" circle @click="handleEdit(scope.row)"><el-icon><Edit /></el-icon></el-button>
<el-button size="small" circle @click="handleView(scope.row)"><el-icon><ZoomIn /></el-icon></el-button>
<el-button size="small" type="danger" circle @click="handleRemove(scope.row)"><el-icon><Delete /></el-icon></el-button>
</template>
</el-table-column>
</el-table>
<div class="page-pagination">
<el-pagination
:current-page="page"
background
layout="prev, pager, next, sizes, total"
:total="total"
:page-sizes="[10, 50, 100]"
:page-size="pageSize"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"></el-pagination>
</div>
</el-card>
<el-dialog v-model="showDialog" width="900px" :title="opaType == 'view' ? '查看详情' : opaType == 'add' ? '新增' : '编辑' " @close="resetForm">
<el-form label-position="right" label-width="110px" :disabled="opaType == 'view'">
<el-form-item label="商品名称:">
<el-input v-model="itemInfo.title" clearable></el-input>
</el-form-item>
<el-form-item label="商品编码:">
<el-input v-model="itemInfo.goods_code" clearable></el-input>
</el-form-item>
<el-form-item label="商品品牌:">
<el-select v-model="itemInfo.brand_id" placeholder="请选择" clearable>
<el-option v-for="it in brandList" :key="it.id" :label="it.name" :value="it.id" />
</el-select>
</el-form-item>
<el-form-item label="商品图片:">
<el-upload
ref="uploadRef"
action="/api/upload/img"
:headers="headers"
:on-remove="handleRemoveImg"
:on-error="handleUploadError"
:on-preview="handlePreview"
:on-success="handleSuccess"
:file-list="fileList"
:show-file-list="true"
list-type="picture-card"
accept=".png,.jpg,.gif">
<el-icon><Plus /></el-icon>
</el-upload>
</el-form-item>
<el-form-item label="仓库:">
<el-select v-model="itemInfo.warehouse_id" placeholder="请选择" clearable>
<el-option v-for="it in warehouseList" :key="it.id" :label="it.name" :value="it.id" />
</el-select>
</el-form-item>
<el-form-item label="说明:">
<el-input v-model="itemInfo.introduce" type="textarea" :rows="4"></el-input>
</el-form-item>
<el-form-item label="商品状态:">
<el-radio-group v-model="itemInfo.status">
<el-radio :label="1">启用</el-radio>
<el-radio :label="0">不启用</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="排序:">
<el-input-number v-model="itemInfo.sort" :min="0" />
</el-form-item>
</el-form>
<div v-for="(item, i) in skusList" :key="i" class="skuBox">
<div class="tit">规格{{ i + 1 }}</div>
<el-form label-width="110px" :inline="true" :disabled="opaType == 'view'">
<el-form-item label="规格名称:">
<el-input placeholder="规格名称" v-model="item.title" clearable style="width: 250px;"></el-input>
</el-form-item>
<el-form-item label="规格编码:">
<el-input placeholder="规格编码" v-model="item.sku_code" clearable style="width: 250px;"></el-input>
</el-form-item>
<el-form-item label="仓库:">
<el-select v-model="item.warehouse_id" placeholder="请选择" clearable style="width: 250px;">
<el-option v-for="it in warehouseList" :key="it.id" :label="it.name" :value="it.id" />
</el-select>
</el-form-item>
<el-form-item label="商品状态:">
<el-radio-group v-model="item.status">
<el-radio :label="1">启用</el-radio>
<el-radio :label="0">不启用</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="赠品:">
<el-radio-group v-model="item.gift" style="width: 250px;">
<el-radio :label="0">不是</el-radio>
<el-radio :label="1"></el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="排序:">
<el-input-number v-model="item.sort" :min="0" />&nbsp;&nbsp;&nbsp;&nbsp;
<el-button type="danger" v-if="opaType != 'view'" @click="handleSkuDelete(i)" size="small"><el-icon><Delete /></el-icon>&nbsp;删除</el-button>
</el-form-item>
</el-form>
</div>
<el-form label-position="right" label-width="110px" v-if="opaType != 'view'">
<el-form-item label="">
<el-button type="success" @click="toAddSku()">增加规格</el-button>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer" v-if="opaType != 'view'">
<el-button @click="showDialog = false">取消</el-button>
<el-button type="primary" @click="commitOpa()" :loading="opa_loading">确定</el-button>
</span>
</template>
</el-dialog>
<!-- 预览图片 -->
<el-dialog v-model="picVisible" center width="800px">
<img :src="dialogImageUrl" style="max-width: 700px; margin: 0 auto;display: block;" />
</el-dialog>
</div>
</template>
<script>
import { onMounted, reactive, toRefs, ref, nextTick } from "vue"
import { get, post } from "@/api/request"
import { Search, Plus, Edit, ZoomIn, Delete } from '@element-plus/icons'
import { ElMessage, ElMessageBox } from 'element-plus'
import { parseErrors } from 'components/common'
export default {
components: {
Search, Plus, Edit, ZoomIn, Delete
},
setup() {
const data = reactive({
filter: {},
goodsList: [],
page: 1,
pageSize: 10,
total: 0,
loading: false,
opaType: '',
showDialog: false,
opa_loading: false,
headers: {
Authorization: localStorage.getItem('token'),
'Shop-Id': localStorage.getItem('shopId') || ''
},
picVisible: false,
dialogImageUrl: '',
fileList: [],
brandList: [],
warehouseList: [],
itemInfo: {},
skusList: []
})
function handleSearch() {
data.page = 1
fetchData()
}
const fetchData = () => {
data.loading = true
let params = {
page: data.page,
pageSize: data.pageSize,
...data.filter
}
get(`/api/goods`, params).then((res) => {
data.goodsList = res.data
data.total = res.meta.total
data.loading = false
}).catch(() => {
data.loading = false
})
}
function handleCurrentChange(e) {
data.page = e
fetchData()
}
function handleSizeChange(e) {
data.page = 1
data.pageSize = e
fetchData()
}
function handleAdd() {
data.opaType = 'add'
data.fileList = []
data.skusList = []
data.itemInfo = {
status: 1,
sort: 0
}
data.showDialog = true
}
function handleEdit(item) {
data.opaType = 'edit'
get(`/api/goods/${item.id}`).then((res) => {
data.itemInfo = res.data
data.fileList = []
if(res.data.images && res.data.images.length) {
res.data.images.forEach((it) => {
data.fileList.push({url: it})
})
}
data.skusList = res.data.skus || []
data.showDialog = true
})
}
function handleView(item) {
data.opaType = 'view'
get(`/api/goods/${item.id}`).then((res) => {
data.itemInfo = res.data
data.fileList = []
if(res.data.images && res.data.images.length) {
res.data.images.forEach((it) => {
data.fileList.push({url: it})
})
}
data.skusList = res.data.skus || []
data.showDialog = true
})
}
function commitOpa() {
data.opa_loading = true
let imgs = []
data.fileList.forEach((it) => {
imgs.push(it.url)
})
let params = {
...data.itemInfo
}
params.skus = data.skusList
params.images = imgs
if(data.opaType == 'add') {
post(`/api/goods`, params).then(() => {
data.page = 1
fetchData()
ElMessage({ type: 'success', message: '新增成功' })
data.showDialog = false
data.opa_loading = false
}).catch((err) => {
data.opa_loading = false
parseErrors(err.response.data.errors)
})
} else {
post(`/api/goods/${data.itemInfo.id}`, params, 'PUT').then(() => {
fetchData()
ElMessage({ type: 'success', message: '编辑成功' })
data.showDialog = false
data.opa_loading = false
}).catch((err) => {
data.opa_loading = false
parseErrors(err.response.data.errors)
})
}
}
function handleRemoveImg(res, ress) {
data.fileList = ress
}
function handleUploadError(err) {
if(JSON.parse(err.message) && JSON.parse(err.message).message) {
ElMessage({ type: 'error', message: JSON.parse(err.message).message })
}
}
function handlePreview(File) {
data.dialogImageUrl = File.url
data.picVisible = true
}
function handleSuccess(res) {
data.fileList.push({url: res.data.link})
}
function handleSkuDelete(index) {
data.skusList.splice(index, 1)
}
const uploadRef = ref(null)
const resetForm = () => {
data.fileList = []
nextTick(() => {
uploadRef.value.clearFiles()
})
}
function toAddSku() {
let sku = {
title: '',
sku_code: '',
warehouse_id: data.itemInfo.warehouse_id || '',
status: 1,
sort: 0,
gift: 0
}
data.skusList.push(sku)
}
function handleRemove(item) {
ElMessageBox.confirm('确定要删除当前数据吗?', '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
post(`/api/goods/${item.id}`, {}, 'DELETE').then(() => {
getGroupList()
ElMessage({ type: 'success', message: '删除成功' })
})
})
}
function getBrandList() {
get(`/api/all/brands`).then((res) => {
data.brandList = res.data
})
}
function getWarehouseList() {
get(`/api/all/warehouses`).then((res) => {
data.warehouseList = res.data
})
}
onMounted(() => {
fetchData()
getBrandList()
getWarehouseList()
})
return {
uploadRef,
...toRefs(data),
handleSearch,
handleCurrentChange,
handleSizeChange,
fetchData,
handleAdd,
handleEdit,
commitOpa,
handleRemove,
handleUploadError,
handlePreview,
handleSuccess,
handleView,
handleSkuDelete,
resetForm,
toAddSku,
handleRemoveImg,
getBrandList,
getWarehouseList
}
}
}
</script>
<style lang="scss" scoped>
.searchBox{
display: flex;
flex-wrap: wrap;
background-color: #fff;
padding: 15px 0 0 0;
border-radius: 4px;
margin-bottom: 15px;
.row{
display: flex;
align-items: center;
width: 20%;
box-sizing: border-box;
margin-bottom: 15px;
&.row1{
width: 25%;
}
.span{
display: block;
width: 80px;
font-size: 14px;
text-align: right;
box-sizing: border-box;
}
.right{
width: calc(100% - 100px);
}
.wid100{
width: 100%;
}
}
}
.opaBox{
margin-bottom: 15px;
}
.imgBox{
display: flex;
flex-wrap: wrap;
.el-image{
width: 60px;
height: 60px;
border-radius: 4px;
margin-right: 10px;
}
}
.skuBox{
border: 1px solid #e5e5e5;
border-radius: 5px;
padding: 15px 0;
margin-bottom: 15px;
background-color: #f3f3f3;
.tit{
padding-left: 40px;
font-weight: 600;
font-size: 15px;
margin-bottom: 15px;
}
}
</style>

358
src/views/commodity/sku.vue Normal file
View File

@ -0,0 +1,358 @@
<template>
<div class="pageBox">
<div class="searchBox">
<div class="row">
<span class="span">商品</span>
<div class="right">
<el-select v-model="filter.goods_id" placeholder="请选择" clearable filterable class="wid100">
<el-option v-for="it in goodsList" :key="it.id" :label="it.title" :value="it.id" />
</el-select>
</div>
</div>
<div class="row">
<span class="span">规格名称</span>
<div class="right"><el-input v-model="filter.title" class="wid100" clearable></el-input></div>
</div>
<div class="row">
<span class="span">规格编码</span>
<div class="right"><el-input v-model="filter.sku_code" class="wid100" clearable></el-input></div>
</div>
<div class="row">
<span class="span">仓库</span>
<div class="right">
<el-select v-model="filter.warehouse_id" placeholder="请选择" clearable class="wid100" filterable>
<el-option v-for="it in warehouseList" :key="it.id" :label="it.name" :value="it.id" />
</el-select>
</div>
</div>
<div class="row">
<span class="span">状态</span>
<div class="right">
<el-select v-model="filter.status" placeholder="请选择" clearable class="wid100">
<el-option label="启用" :value="1" />
<el-option label="不启用" :value="0" />
</el-select>
</div>
</div>
<div class="row">
<span class="span"></span>
<div class="right"><el-button type="primary" @click="handleSearch"><el-icon><Search /></el-icon>&nbsp;筛选</el-button></div>
</div>
</div>
<el-card shadow="never">
<div class="opaBox">
<el-button type="primary" @click="handleAdd"><el-icon><Plus /></el-icon>&nbsp;新增</el-button>
<!-- <el-button type="warning" @click="handleExport"><span class="iconfont icon-daochu"></span>&nbsp;导出</el-button> -->
</div>
<el-table :data="tableList" style="width: 100%" border v-loading="loading">
<el-table-column prop="id" label="ID" width="80" align="center" />
<el-table-column prop="goods.title" label="商品名称" align="center" />
<el-table-column prop="goods.goods_code" label="商品货号" align="center" />
<el-table-column prop="title" label="规格名称" align="center" />
<el-table-column prop="sku_code" label="规格编码" align="center" />
<el-table-column prop="actual_inventory" label="实际库存" align="center" />
<el-table-column prop="total_lock_num" label="总锁定库存" align="center" />
<el-table-column prop="lock_in_stock" label="运营锁定库存" align="center" />
<el-table-column prop="after_sale_stock" label="售后锁定库存" align="center" />
<el-table-column prop="available_inventory" label="可售库存" align="center" />
<el-table-column prop="warehouse.name" label="仓库" align="center" />
<el-table-column label="状态" align="center">
<template #default="scope">
<span>{{ scope.row.status ? '启用' : '不启用' }}</span>
</template>
</el-table-column>
<el-table-column prop="created_at" label="添加时间" align="center" />
<el-table-column label="操作" align="center" width="150">
<template #default="scope">
<el-button type="primary" circle @click="handleEdit(scope.row)"><el-icon><Edit /></el-icon></el-button>
</template>
</el-table-column>
</el-table>
<div class="page-pagination">
<el-pagination
:current-page="page"
background
layout="prev, pager, next, sizes, total"
:total="total"
:page-sizes="[10, 50, 100]"
:page-size="pageSize"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"></el-pagination>
</div>
</el-card>
<el-dialog v-model="showDialog" width="600px" :title="opaType == 'add' ? '新增' : '编辑' ">
<el-form label-position="right" label-width="110px">
<el-form-item label="所属商品:">
<el-select v-model="itemInfo.goods_id" placeholder="请选择" clearable filterable :disabled="role && opaType == 'edit'">
<el-option v-for="it in goodsList" :key="it.id" :label="it.title" :value="it.id" />
</el-select>
</el-form-item>
<el-form-item label="规格名称:">
<el-input v-model="itemInfo.title" clearable :disabled="role && opaType == 'edit'"></el-input>
</el-form-item>
<el-form-item label="规格编码:">
<el-input v-model="itemInfo.sku_code" clearable :disabled="role && opaType == 'edit'"></el-input>
</el-form-item>
<el-form-item label="仓库:">
<el-select v-model="itemInfo.warehouse_id" placeholder="请选择" clearable filterable :disabled="role && opaType == 'edit'">
<el-option v-for="it in warehouseList" :key="it.id" :label="it.name" :value="it.id" />
</el-select>
</el-form-item>
<el-form-item label="总锁定库存:">
<el-input v-model="itemInfo.total_lock_num" disabled></el-input>
</el-form-item>
<el-form-item label="运营锁定库存:">
<el-input v-model="itemInfo.lock_in_stock" clearable></el-input>
</el-form-item>
<el-form-item label="售后锁定库存:">
<el-input v-model="itemInfo.after_sale_stock" clearable :disabled="role && opaType == 'edit'"></el-input>
</el-form-item>
<el-form-item label="状态:">
<el-radio-group v-model="itemInfo.status" :disabled="role && opaType == 'edit'">
<el-radio :label="1">启用</el-radio>
<el-radio :label="0">不启用</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="赠品:">
<el-radio-group v-model="itemInfo.gift" :disabled="role && opaType == 'edit'">
<el-radio :label="0">不是</el-radio>
<el-radio :label="1"></el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="排序:">
<el-input-number v-model="itemInfo.sort" :min="0" :disabled="role && opaType == 'edit'" />
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer" v-if="opaType != 'view'">
<el-button @click="showDialog = false">取消</el-button>
<el-button type="primary" @click="commitOpa()" :loading="opa_loading">确定</el-button>
</span>
</template>
</el-dialog>
<!-- 预览图片 -->
<el-dialog v-model="picVisible" center width="800px">
<img :src="dialogImageUrl" style="max-width: 700px; margin: 0 auto;display: block;" />
</el-dialog>
</div>
</template>
<script>
import { onMounted, reactive, toRefs } from "vue"
import { get, post } from "@/api/request"
import { Search, Plus, Edit, ZoomIn, Delete } from '@element-plus/icons'
import { ElMessage } from 'element-plus'
import { parseErrors } from 'components/common'
export default {
components: {
Search, Plus, Edit, ZoomIn, Delete
},
setup() {
const data = reactive({
filter: {},
tableList: [],
page: 1,
pageSize: 10,
total: 0,
loading: false,
opaType: '',
showDialog: false,
opa_loading: false,
warehouseList: [],
goodsList: [],
itemInfo: {},
role: localStorage.getItem('roleName') == '爆品运营'
})
function handleSearch() {
data.page = 1
fetchData()
}
const fetchData = () => {
data.loading = true
let params = {
page: data.page,
pageSize: data.pageSize,
service_id: data.service_id,
...data.filter
}
get(`/api/goods-skus`, params).then((res) => {
data.tableList = res.data
data.total = res.meta.total
data.loading = false
}).catch(() => {
data.loading = false
})
}
function handleCurrentChange(e) {
data.page = e
fetchData()
}
function handleSizeChange(e) {
data.page = 1
data.pageSize = e
fetchData()
}
function handleAdd() {
data.opaType = 'add'
data.itemInfo = {
status: 1,
sort: 0,
gift: 0
}
data.showDialog = true
}
function handleEdit(item) {
data.opaType = 'edit'
data.itemInfo = JSON.parse(JSON.stringify(item))
data.showDialog = true
}
function commitOpa() {
data.opa_loading = true
let params = {
...data.itemInfo
}
if(data.opaType == 'add') {
post(`/api/goods-skus`, params).then(() => {
data.page = 1
fetchData()
ElMessage({ type: 'success', message: '新增成功' })
data.showDialog = false
data.opa_loading = false
}).catch((err) => {
data.opa_loading = false
parseErrors(err.response.data.errors)
})
} else {
if(data.role) {
params = {
lock_in_stock: data.itemInfo.lock_in_stock
}
post(`/api/goods/sku/lockInStock/${data.itemInfo.id}`, params, 'PUT').then(() => {
fetchData()
ElMessage({ type: 'success', message: '编辑成功' })
data.showDialog = false
data.opa_loading = false
}).catch((err) => {
data.opa_loading = false
parseErrors(err.response.data.errors)
})
} else {
post(`/api/goods-skus/${data.itemInfo.id}`, params, 'PUT').then(() => {
fetchData()
ElMessage({ type: 'success', message: '编辑成功' })
data.showDialog = false
data.opa_loading = false
}).catch((err) => {
data.opa_loading = false
parseErrors(err.response.data.errors)
})
}
}
}
function getWarehouseList() {
get(`/api/all/warehouses`).then((res) => {
data.warehouseList = res.data
})
}
function getGoodsList() {
get(`/api/all/goods`).then((res) => {
data.goodsList = res.data
})
}
onMounted(() => {
fetchData()
getWarehouseList()
getGoodsList()
})
return {
...toRefs(data),
handleSearch,
handleCurrentChange,
handleSizeChange,
fetchData,
handleAdd,
handleEdit,
commitOpa,
getWarehouseList,
getGoodsList
}
}
}
</script>
<style lang="scss" scoped>
.searchBox{
display: flex;
flex-wrap: wrap;
background-color: #fff;
padding: 15px 0 0 0;
border-radius: 4px;
margin-bottom: 15px;
.row{
display: flex;
align-items: center;
width: 20%;
box-sizing: border-box;
margin-bottom: 15px;
&.row1{
width: 25%;
}
.span{
display: block;
width: 80px;
font-size: 14px;
text-align: right;
box-sizing: border-box;
}
.right{
width: calc(100% - 100px);
}
.wid100{
width: 100%;
}
}
}
.opaBox{
margin-bottom: 15px;
}
.imgBox{
display: flex;
flex-wrap: wrap;
.el-image{
width: 60px;
height: 60px;
border-radius: 4px;
margin-right: 10px;
}
}
.skuBox{
border: 1px solid #e5e5e5;
border-radius: 5px;
padding: 15px 0;
margin-bottom: 15px;
background-color: #f3f3f3;
.tit{
padding-left: 40px;
font-weight: 600;
font-size: 15px;
margin-bottom: 15px;
}
}
</style>

43
src/views/home/index.vue Normal file
View File

@ -0,0 +1,43 @@
<template>
<div class="pageBox">
<div class="oneBox">
<h3>今天也是充满希望的一天</h3>
</div>
</div>
</template>
<script>
import { onMounted, reactive, toRefs } from "vue"
import { get } from "@/api/request"
export default {
setup() {
const data = reactive({
numTotal: {}
})
const fetchData = () => {
// get(`/api/admin/home`).then((res) => {
// data.numTotal = res.data
// })
}
onMounted(() => {
fetchData()
})
return {
...toRefs(data),
fetchData
}
}
}
</script>
<style lang="scss" scoped>
.oneBox{
padding: 20px;
width: 100%;
text-align: center;
}
</style>

357
src/views/layout/index.vue Normal file
View File

@ -0,0 +1,357 @@
<template>
<div>
<el-container class="home">
<!--侧边栏-->
<div class="home-aside" :class="hideAside ? 'hideAside' : '' ">
<el-scrollbar wrap-class="scrollbar-wrapper">
<Menu :menu-data="menuData" :is-collapse="hideAside"></Menu>
</el-scrollbar>
</div>
<el-main class="home-main" :class="hideAside ? 'hideAside' : '' ">
<div class="main">
<div class="topHead">
<div class="expand" @click="hideAside = !hideAside">
<el-icon v-if="hideAside"><Expand /></el-icon>
<el-icon v-else><Fold /></el-icon>
</div>
<!-- <Breadcrumb class="breadcrumb" separator="/"></Breadcrumb> -->
<el-dropdown @command="(command) => { handleCommand(command); }">
<div style="display: flex;padding-right: 20px;">
<span class="el-dropdown-link"><el-icon style="vertical-align: bottom;"><UserFilled /></el-icon>&nbsp;{{ name }}</span>
<el-icon><ArrowDown /></el-icon>
</div>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="1"><el-icon><SwitchButton /></el-icon>退出登录</el-dropdown-item>
<!-- <el-dropdown-item command="2"><el-icon><Unlock /></el-icon>修改密码</el-dropdown-item> -->
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
<router-view />
<!-- <router-view v-slot="{ Component }">
<Transition mode="out-in">
<component :is="Component" />
</Transition>
</router-view> -->
<!-- <router-view v-slot="{ Component, route }">
<transition mode="out-in" appear>
<component :is="Component" :key="route.path" />
</transition>
</router-view> -->
</div>
</el-main>
</el-container>
<el-dialog v-model="showModify" title="修改密码" width="350px">
<el-form label-position="right" label-width="85px">
<el-form-item label="修改密码:">
<el-input v-model="item.password" type="password"></el-input>
</el-form-item>
<el-form-item label="确认密码:">
<el-input v-model="item.password_confirmation" type="password"></el-input>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="showModify = false">取消</el-button>
<el-button type="primary" @click="commitModify">确定</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script>
import { onMounted, reactive, toRefs, watch } from 'vue'
import { routes } from '@/router'
import Menu from './menu'
import Breadcrumb from '@/components/Breadcrumb'
import { SwitchButton, ArrowDown, Unlock, UserFilled, Expand, Fold } from '@element-plus/icons'
import { useRouter } from 'vue-router'
import { get, post } from "@/api/request"
import { ElMessage } from 'element-plus'
export default {
components: {
Menu,
Breadcrumb,
SwitchButton,
ArrowDown,
Unlock,
Expand,
Fold,
UserFilled
},
setup() {
const data = reactive({
title: '',
name: localStorage.getItem('userName'),
item: {},
menuData: [],
hideAside: false,
showModify: false
})
const router = useRouter()
//
watch(
() => router.currentRoute.value.meta.title,
(toPath, old) => {
data.title = toPath
},
{
deep: true,
immediate: true
}
)
const NewRoutes = (() => {
const retRoutes = []
for (const value of routes) {
if (value.path === '/') {
if (value.children && value.children.length) {
// children,便<SidebarItem>
for (const routeItem of value.children) {
const obj = Object.assign({}, routeItem)
obj.path = `/${routeItem.path}`
if(obj.name == 'manage') {
obj.hidden = localStorage.getItem('isSuper') == 0
}
retRoutes.push(obj)
}
}
} else {
retRoutes.push(value)
}
}
console.log(retRoutes)
return retRoutes
})
const handleCommand = (type) => {
if (type == 1) {
loginOut()
} else if(type == 2) {
data.item = {}
data.showModify = true
}
}
const commitModify = () => {
post('/api/staffs/password', data.item).then(() => {
ElMessage({ type: 'success', message: '修改成功' })
data.showModify = false
loginOut()
})
}
const loginOut = () => {
get('/api/auth/logout').then(() => {
localStorage.removeItem('token')
localStorage.removeItem('saveTime')
router.replace({
path: '/login'
})
})
}
onMounted(async () => {
data.menuData = JSON.parse(localStorage.getItem('menusList'))
})
return {
...toRefs(data),
handleCommand,
commitModify
}
}
}
</script>
<style lang="scss" scoped>
.breadcrumb{
padding: 0 20px;
}
.topHead{
height: 45px;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid #E5E5E5;
background-color: #fff;
.expand{
padding: 10px;
box-sizing: border-box;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
margin-right: 15px;
cursor: pointer;
}
}
.home {
position: relative;
height: 100vh;
width: 100vw;
overflow: hidden;
display: block;
&-header {
background-color: #fff;
display: flex;
justify-content: space-between;
// position: absolute;
border-bottom: 1px solid #e5e5e5;
box-sizing: border-box;
top: 0;
left: 0;
width: 100%;
padding: 0;
&-title {
display: flex;
align-items: center;
// justify-content: center;
padding-left: 17px;
box-sizing: border-box;
background-color: #FAFAFA;
color: #fff;
width: 100%;
height: 40px;
white-space: nowrap;
}
&-right {
display: flex;
justify-content: space-between;
flex: 1;
.expand{
padding: 10px;
box-sizing: border-box;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
cursor: pointer;
}
&-btn {
height: 40px;
// border-bottom: 1px solid #ececec;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 20px;
}
}
}
&-aside {
height: 100%;
width: 200px;
background-color: #F9FAFC;
transition: width 0.3s;
position: fixed;
overflow: hidden;
top: 0;
bottom: 0;
left: 0;
z-index: 1;
.home-header-title{
width: 200px;
}
&.hideAside{
width: 64px;
.home-header-title{
padding: 0;
width: 64px;
justify-content: center;
}
}
&-menu {
border: none;
}
}
&-main {
position: relative;
transition: margin-left 0.28s;
margin-left: 200px;
padding: 0;
box-sizing: border-box;
height: 100%;
background-color: #ecf0f5;
&.hideAside{
// width: calc(100% - 64px);
margin-left: 64px;
}
.main {
box-sizing: border-box;
}
}
}
.el-icon {
&:hover{
color: #409eff;
}
}
.el-menu{
border-right: none;
}
.v-enter-active,
.v-leave-active {
transition: all 0.5s ease;
}
.v-leave-to,
.v-enter-from {
opacity: 0;
transform: translateX(-30px);
}
.v-leave-from,
.v-enter-to {
transform: translateX(0px);
}
</style>
<style>
.home-header-right-btn .el-dropdown--small{
display: flex;
align-items: center;
}
.home-aside .el-aside{
height: calc(100vh - 40px);
overflow-x: hidden;
}
.el-menu-item{
height: 50px !important;
line-height: 50px !important;
}
.el-sub-menu__title{
width: 200px;
height: 50px !important;
line-height: 50px !important;
}
.hideAside .el-sub-menu__title{
width: 64px;
}
.home-aside .el-sub-menu .el-sub-menu__icon-arrow{
width: 12px;
}
.el-sub-menu .el-menu-item{
padding: 0 0 0 45px !important;
}
.el-menu--popup .el-sub-menu .el-menu-item{
padding: 0 0 0 20px !important;
}
.el-menu .el-sub-menu .el-menu .el-sub-menu .el-menu .el-menu-item{
padding: 0 0 0 55px !important;
}
.el-menu--popup .el-sub-menu__icon-arrow{
width: 12px !important;
}
</style>

43
src/views/layout/menu.vue Normal file
View File

@ -0,0 +1,43 @@
<template>
<el-menu :default-active="activerouter" class="el-menu-vertical-demo" background-color="#FAFAFA"
text-color="#232323" router unique-opened :collapse="isCollapse" :collapse-transition="false">
<menu-tree :menu-data="menuData" />
</el-menu>
</template>
<script>
import { onMounted, reactive, toRefs } from 'vue'
import menuTree from './menuTree'
export default {
props: {
menuData: Array,
isCollapse: {
type: Boolean,
default: false
}
},
components: {
menuTree,
},
setup() {
const data = reactive({
activerouter: ''
})
onMounted(() => {
let defaultMenu = window.location.hash.substr(
window.location.hash.indexOf("/")
)
data.activerouter = defaultMenu
})
return {
...toRefs(data)
}
},
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,58 @@
<template>
<template v-for="(menu, i) in menuData" :key="menu.uri">
<el-menu-item :index="menu.uri" v-if="!menu.children">
<div class="icon" v-if="menu.parent_id == 0">
<img v-if="menu.icon" :src="menu.icon">
</div>
<template #title><span :class="menu.parent_id == 0 ? 'span' : ''">{{menu.title}}</span></template>
</el-menu-item>
<el-sub-menu :index="menu.uri" v-else popper-class="popper-meun">
<template #title>
<div class="icon" v-if="menu.parent_id == 0">
<img v-if="menu.icon" :src="menu.icon">
</div>
<span :class="menu.parent_id == 0 ? 'span' : 'sp1'">{{menu.title}}</span>
</template>
<menu-tree :menu-data="menu.children" />
</el-sub-menu>
</template>
</template>
<script>
export default {
props: {
menuData: Array
},
setup() {
return {}
}
}
</script>
<style scoped lang="scss">
.icon{
width: 24px;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
position: absolute;
left: 20px;
top: 0;
}
.icon img{
width: 17px;
}
.sp1{
padding-left: 10px;
}
</style>
<style scoped>
.el-menu-item ::v-deep .span, ::v-deep .el-sub-menu__title .span{
padding-left: 30px;
position: relative;
z-index: 10;
}
</style>

45
src/views/login/404.vue Normal file
View File

@ -0,0 +1,45 @@
<template>
<div class="whole">
<div class="notfund">
<img class="img" src="@/assets/imgs/404.jpg" />
<div class="box">
<p>抱歉您访问的页面找不到了/(ㄒoㄒ)/~~</p>
<el-button type="primary" @click="$router.back()">返回上一页</el-button>
</div>
</div>
</div>
</template>
<script>
import { useRouter } from 'vue-router'
export default {
setup() {
const router = useRouter()
function toBackPage() {
}
return {
toBackPage
}
}
}
</script>
<style lang="scss" scoped>
.whole{
width: 100%;
height: 100vh;
.notfund{
width: 100%;
position: relative;
text-align: center;
}
.box{
position: absolute;
width: 100%;
bottom: 0;
}
}
</style>

212
src/views/login/index.vue Normal file
View File

@ -0,0 +1,212 @@
<template>
<div class="login">
<div class="login-box">
<div class="login-box-title">管理系统</div>
<el-input v-model="username" placeholder="用户名" class="account" />
<el-input class="password" v-model="password" placeholder="密码" show-password @keyup.enter.native="Login" />
<el-button type="primary" @click="Login" :loading="loading">登录</el-button>
</div>
</div>
</template>
<script>
import { reactive, toRefs, onMounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { ElMessage } from 'element-plus'
import { post } from '@/api/request'
import axios from 'axios'
import { whiteList } from '@/router/white'
export default {
setup() {
const router = useRouter()
const data = reactive({
username: '',
password: '',
loading: false
})
async function Login() {
if(!data.username || !data.password) {
return ElMessage({ type: 'warning', message: '请输入用户名及密码' })
}
data.loading = true
await post('/api/auth/login', { username: data.username, password: data.password }).then(async(res) => {
if (res.access_token) {
const token = res.token_type + ' ' + res.access_token
localStorage.setItem('token', token)
localStorage.setItem('saveTime', Date.now()) //
localStorage.setItem('expires_in', (res.expires_in - 10) * 1000) //
if(res.user.role_name == '销售主账号') {
localStorage.setItem('roleName', '销售')
localStorage.setItem('roleNamex', '销售x')
} else {
localStorage.setItem('roleName', res.user.role_name)
localStorage.setItem('roleNamex', '')
}
localStorage.setItem('userName', data.username)
await getMenusList()
router.replace('/home')
}
setTimeout(() => {
data.loading = false
}, 1000)
}).catch(() => {
data.loading = false
})
}
function parseToken(token) {
axios.get(`/api/authInfo`, {
headers: {
'Authorization': token,
'content-type': 'application/json'
}
}).then(async(res) => {
const token = res.data.token_type + ' ' + res.data.access_token
localStorage.setItem('token', token)
localStorage.setItem('saveTime', Date.now()) //
localStorage.setItem('expires_in', (res.data.expires_in - 10) * 1000) //
if(res.data.user.role_name == '销售主账号') {
localStorage.setItem('roleName', '销售')
localStorage.setItem('roleNamex', '销售x')
} else {
localStorage.setItem('roleName', res.data.user.role_name)
localStorage.setItem('roleNamex', '')
}
localStorage.setItem('userName', res.data.user.username)
await getMenusList()
router.replace('/home')
})
}
//
let menus = []
async function getMenusList() {
await post('/api/role/menus').then((res) => {
localStorage.setItem('menusList', JSON.stringify(res.data))
res.data.forEach(async(item) => {
if(item.children) {
await fn(item.children)
} else {
menus.push({
path: item.uri,
name: getRouterName(item.uri),
full_path: getRouterFullPath(item.uri)
})
}
})
localStorage.setItem('menus', JSON.stringify(menus))
})
}
function getRouterName(uri) {
let name = ''
whiteList.forEach((item) => {
if(uri == item.path) {
name = item.name
}
})
return name
}
function getRouterFullPath(uri) {
let full_path = ''
whiteList.forEach((item) => {
if(uri == item.path) {
full_path = item.full_path
}
})
return full_path
}
function fn(item) {
return new Promise((resolve, reject) => {
item.forEach(async(item) => {
if(item.children) {
await fn(item.children)
} else {
menus.push({
path: item.uri,
name: getRouterName(item.uri),
full_path: getRouterFullPath(item.uri)
})
}
})
})
}
const route = useRoute()
onMounted(() => {
if(route.query.token) {
parseToken(route.query.token)
} else {
let token = localStorage.getItem('token')
let saveTime = localStorage.getItem('saveTime')
let expires_in = localStorage.getItem('expires_in')
if(token && saveTime && expires_in) {
if((Date.now() > saveTime + expires_in)) {
localStorage.removeItem('token')
localStorage.removeItem('saveTime')
localStorage.removeItem('expires_in')
router.replace({ path: '/login' })
} else {
goToPage()
}
} else {
router.replace({ path: '/login' })
}
}
})
function goToPage() {
router.replace({ path: '/home' })
}
return {
...toRefs(data),
Login,
goToPage,
parseToken
}
}
}
</script>
<style lang="scss" scoped>
.login {
height: 100%;
width: 100%;
&-box {
width: 300px;
position: absolute;
left: 50%;
top: 30%;
transform: translate(-30%, -50%);
display: flex;
flex-direction: column;
&-title {
text-align: center;
font-size: 25px;
margin-bottom: 30px;
}
.el-input {
margin-bottom: 20px;
}
}
}
.wechat{
width: 100%;
display: flex;
align-items: center;
justify-content: center;
.img{
width: 30px;
cursor: pointer;
}
}
</style>

View File

@ -0,0 +1,25 @@
import {reactive} from "vue";
export default function menu() {
const menuData = reactive({
treeData: '',
treeDataValue: {},
defaultProps: {
parent: 'parent_id', // 父级唯一标识
value: 'id', // 唯一标识
label: 'title', // 标签显示
children: 'children' // 子级
},
})
// 选择
function handleNodeClick (data){
menuData.treeData = data.id
menuData.treeDataValue = data
console.log(data)
}
return {
menuData,
handleNodeClick
}
}

View File

@ -0,0 +1,263 @@
<template>
<div class="pageBox">
<el-card shadow="never">
<div class="opaBox">
<el-button type="primary" @click="addBtn"><el-icon><Plus /></el-icon>&nbsp;新增</el-button>
</div>
<el-table :data="tableData" border style="width: 100%" row-key="id" v-loading="loading">
<el-table-column prop="title" label="名称" width="200" />
<el-table-column prop="id" label="ID" width="150" />
<el-table-column prop="order" label="排序" sortable width="150" />
<el-table-column prop="uri" label="路径" />
<el-table-column label="图标">
<template #default="scope">
<div class="iconBox" v-if="scope.row.icon">
<el-image :src="scope.row.icon" class="icon"></el-image>
</div>
</template>
</el-table-column>
<el-table-column label="状态" width="180">
<template #default="scope">
<el-switch
v-model="scope.row.is_enabled"
:active-text="scope.row.is_enabled ? '启用' : ''"
@change="changeType(scope.row)" />
</template>
</el-table-column>
<el-table-column label="操作" fixed="right" width="200" align="center">
<template #default="scope">
<el-button type="primary" @click="editBtn(scope.row)">编辑</el-button>
<el-button type="danger" @click="delBtn(scope.row.id)">删除</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<el-dialog v-model="addDialog" :title="item.id ? '编辑菜单' : '添加菜单'" width="500px">
<el-form :model="item" label-position="right" label-width="100px">
<el-form-item label="上级菜单">
<el-select v-model="treeDataValue.id" placeholder="请选择" clearable>
<el-option :value="treeDataValue.id" :label="treeDataValue.title" style="height: auto;">
<el-tree
:data="tableData"
ref="tree"
node-key="id"
:highlight-current="true"
:props="defaultProps"
@node-click="handleNodeClick" />
</el-option>
</el-select>
</el-form-item>
<el-form-item label="菜单名称">
<el-input v-model="item.title" style="width: 200px;" clearable></el-input>
</el-form-item>
<el-form-item label="菜单图标">
<el-upload
class="avatar-uploader"
action="/api/upload/img"
:on-error="handleUploadError"
:headers="headers"
:on-remove="handleRemove"
:on-preview="handlePreview"
:on-success="handleSuccess"
:file-list="iconImage"
:show-file-list="true"
list-type="picture-card"
:limmit="1"
accept=".png,.jpg,.gif">
<el-icon><Plus /></el-icon>
</el-upload>
</el-form-item>
<el-form-item label="路由地址">
<el-input v-model="item.uri" style="width: 200px;" clearable></el-input>
</el-form-item>
<el-form-item label="排序">
<el-input v-model="item.order" style="width: 200px;" clearable></el-input>
</el-form-item>
<el-form-item label="状态">
<el-switch v-model="item.is_enabled" :active-text="item.is_enabled ? '启用' : ''" />
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="close">取消</el-button>
<el-button type="primary" @click="submitBtn">确定</el-button>
</span>
</template>
</el-dialog>
<!-- 预览图片 -->
<el-dialog v-model="picVisible" center width="800px">
<img :src="dialogImageUrl" style="max-width: 700px; margin: 0 auto;display: block;" />
</el-dialog>
</div>
</template>
<script>
import { onMounted, reactive, toRefs } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { get, post } from '@/api/request'
import menu from './menu'
import { Plus } from '@element-plus/icons'
export default {
components: {
Plus
},
setup() {
const data = reactive({
tableData: [],
loading: false,
item: {},
addDialog: false,
headers: {
Authorization: localStorage.getItem('token'),
'Shop-Id': localStorage.getItem('shopId') || ''
},
dialogImageUrl: '',
picVisible: false,
iconImage: []
})
function getMenuList() {
data.loading = true
get('/api/menu').then((res) => {
data.tableData = res.data
data.loading = false
}).catch(() => {
data.loading = false
})
}
const { menuData, handleNodeClick } = menu()
function addBtn() {
menuData.treeDataValue = {
id: '',
title: ''
}
data.item = {
is_enabled: true,
icon: ''
}
data.iconImage = []
data.addDialog = true
}
function editBtn(item) {
data.item = {
...item
}
data.iconImage = item.icon ? [{url: item.icon}] : []
menuData.treeDataValue = item.parent || {}
data.addDialog = true
}
async function submitBtn() {
data.item.parent_id = menuData.treeDataValue.id || 0
let res = ''
if (data.item.id) {
res = await post(`/api/menu/${data.item.id}`, data.item, 'put')
} else {
res = await post('/api/menu', data.item)
}
if (res.code === 0) {
ElMessage({ type: 'success', message: data.item.id ? '编辑成功' : '添加成功' })
data.addDialog = false
getMenuList()
}
}
async function changeType(item) {
data.item = { ...item }
await post(`/api/menu/${data.item.id}`, data.item, 'put').then(() => {
ElMessage({ type: 'success', message: '编辑成功' })
getMenuList()
})
}
function delBtn(id) {
ElMessageBox.confirm('确定删除该菜单吗?', '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
post(`/api/menu/${id}`, {}, 'delete').then(() => {
ElMessage({ type: 'success', message: '删除成功' })
getMenuList()
})
})
}
function close() {
data.addDialog = false
}
function handleUploadError(err) {
if(JSON.parse(err.message) && JSON.parse(err.message).message) {
ElMessage({ type: 'error', message: JSON.parse(err.message).message })
}
}
function handleRemove(res, ress) {
data.item.icon = ''
data.iconImage = []
}
function handlePreview(File) {
data.dialogImageUrl = File.url
data.picVisible = true
}
function handleSuccess(res) {
if (res.code === 0) {
data.iconImage = [{url: res.data.link}]
data.item.icon = res.data.link
}
}
onMounted(() => {
getMenuList()
})
return {
...toRefs(data),
addBtn,
submitBtn,
close,
editBtn,
delBtn,
changeType,
...toRefs(menuData),
handleNodeClick,
handleUploadError,
handleSuccess,
handlePreview,
handleRemove
}
}
}
</script>
<style lang="scss" scoped>
.opaBox{
margin-bottom: 15px;
}
.iconBox{
background-color: rgba(0,0,0,0.5);
width: 40px;
height: 40px;
border-radius: 4px;
.icon{
width: 24px;
height: 24px;
margin: 8px;
}
}
</style>
<style scoped>
::v-deep .el-upload-list--picture-card .el-upload-list__item{
background-color: rgba(0,0,0,0.5);
}
</style>

View File

@ -0,0 +1,173 @@
<template>
<div class="pageBox">
<el-card shadow="never">
<el-table :data="tableData" border style="width: 100%" v-loading="loading">
<el-table-column prop="name" label="角色名" width="200" />
<el-table-column label="角色类型" width="100">
<template #default="scope">
<span v-if="scope.row.type === 0">主角色</span>
<span v-else>子角色</span>
</template>
</el-table-column>
<el-table-column label="角色权限">
<template #default="scope">
<el-tag v-for="i in scope.row.menus" :key="i.id" size="small" class="md-5">{{i.title}}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" fixed="right" width="200" align="center">
<template #default="scope">
<el-button type="primary" @click="editBtn(scope.row)">编辑</el-button>
</template>
</el-table-column>
</el-table>
<div class="page-pagination">
<el-pagination
:current-page="page"
background
layout="prev, pager, next, sizes, total"
:total="total"
:page-sizes="[10, 50, 100]"
:page-size="pageSize"
@size-change="handleSizeChange"
@current-change="handleCurrentChange" />
</div>
</el-card>
<el-dialog v-model="addDialog" :title="item.id ? '编辑角色' : '新增角色'" width="800px">
<el-form :model="item" label-width="90px">
<el-form-item label="角色名:">
<el-input v-model="item.name" style="width: 400px;" disabled></el-input>
</el-form-item>
<el-form-item label="角色类型:">
<el-select v-model="item.type" placeholder="请选择" disabled>
<el-option label="主角色" :value="0"></el-option>
<el-option label="子角色" :value="1"></el-option>
</el-select>
</el-form-item>
<div style="display: flex;">
<el-form-item label="菜单权限:">
<el-tree ref="tree"
:data="menuList"
show-checkbox
node-key="id"
:props="defaultProps"
:check-strictly="true"
:expand-on-click-node="false"
:default-checked-keys="item.menus" />
</el-form-item>
</div>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="addDialog = false">取消</el-button>
<el-button type="primary" @click="submitBtn">确定</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script>
import { onMounted, reactive, toRefs, nextTick, ref } from 'vue'
import { get, post } from '@/api/request'
import { ElMessage } from 'element-plus'
export default {
setup() {
const data = reactive({
page: 1,
total: 0,
tableData: [],
loading: false,
item: {},
menuList: [],
addDialog: false,
defaultProps: {
parent: 'parent_id', //
value: 'id', //
label: 'title', //
children: 'children', //
},
})
function handleCurrentChange(e) {
data.page = e
getRoleList()
}
function handleSizeChange(e) {
data.page = 1
data.pageSize = e
getRoleList()
}
async function getRoleList() {
data.loading = true
await get('/api/role', { page: data.page, pageSize: 10 }).then((res) => {
data.tableData = res.data
data.total = res.meta.total
data.loading = false
}).catch(() => {
data.loading = false
})
}
async function editBtn(item) {
data.item = { ...item }
let menu = []
item.menus.forEach((i) => {
menu.push(i.id)
})
data.item.menus = menu
await nextTick(async () => {
data.menuList = [...data.menuList]
data.addDialog = true
})
}
let tree = ref(null)
function submitBtn() {
let params = {
roleId: data.item.id,
menus: tree.value.getCheckedKeys()
}
post('/api/role/storeMenus', params).then(() => {
ElMessage({
type: 'success',
message: '编辑成功'
})
data.addDialog = false
getRoleList()
})
}
function getMenuList() {
get('/api/menu').then((res) => {
data.menuList = res.data
})
}
onMounted(() => {
getRoleList()
getMenuList()
})
return {
tree,
...toRefs(data),
handleCurrentChange,
handleSizeChange,
getMenuList,
editBtn,
submitBtn
}
}
}
</script>
<style lang="scss" scoped>
.md-5{
margin: 3px;
}
</style>

View File

@ -0,0 +1,527 @@
<template>
<div class="pageBox">
<!-- <div class="searchBox">
<div class="row">
<span class="span">商品名称</span>
<div class="right"><el-input v-model="goods_name" class="wid100" clearable></el-input></div>
</div>
<div class="row">
<el-button type="primary" @click="handleSearch"><el-icon><Search /></el-icon>&nbsp;筛选</el-button>
</div>
</div> -->
<el-card shadow="never">
<div class="opaBox">
<el-button type="primary" @click="handleAdd"><el-icon><Plus /></el-icon>&nbsp;新增</el-button>
<!-- <el-button type="warning" @click="handleExport"><span class="iconfont icon-daochu"></span>&nbsp;导出</el-button> -->
</div>
<el-table :data="goodsList" style="width: 100%" border v-loading="loading">
<el-table-column prop="id" label="ID" width="80" align="center" />
<el-table-column prop="type" label="采购类型" align="center">
<template #default="scope">
<span>{{ Type[scope.row.type] }}</span>
</template>
</el-table-column>
<el-table-column prop="supplier.name" label="供应商" align="center" />
<el-table-column prop="warehouse.name" label="仓库" align="center" />
<el-table-column prop="batch_number" label="批次" align="center" />
<el-table-column label="采购单" align="center" min-width="150">
<template #default="scope">
<div class="imgBox">
<el-image :z-index="9999" v-for="(item, index) in scope.row.images" :key="index" :src="item" :hide-on-click-modal="true" :preview-src-list="[item]" fit="cover" :preview-teleported="true" />
</div>
</template>
</el-table-column>
<el-table-column prop="attachments" label="附件">
<template #default="scope">
<div class="imgBox">
<a :href="scope.row.attachments" target="_blank" style="word-break: break-all;">{{scope.row.attachments}}</a>
</div>
</template>
</el-table-column>
<el-table-column prop="in_put_time" label="入仓时间" align="center" />
<el-table-column prop="purchasing_agent" label="采购人" align="center" />
<el-table-column prop="admin_user.username" label="创建人" align="center" />
<el-table-column prop="note" label="备注" align="center" />
<el-table-column prop="created_at" label="创建时间" align="center" />
</el-table>
<div class="page-pagination">
<el-pagination
:current-page="page"
background
layout="prev, pager, next, sizes, total"
:total="total"
:page-sizes="[10, 50, 100]"
:page-size="pageSize"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"></el-pagination>
</div>
</el-card>
<el-dialog v-model="showDialog" width="900px" title="新增" @close="resetForm">
<el-form label-position="right" label-width="110px">
<el-form-item label="采购类型:">
<el-radio-group v-model="itemInfo.type">
<el-radio label="default">默认</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="供应商:">
<el-select v-model="itemInfo.supplier_id" placeholder="请选择" clearable filterable>
<el-option v-for="it in suppliersList" :key="it.id" :label="it.name" :value="it.id" />
</el-select>
</el-form-item>
<el-form-item label="仓库:">
<el-select v-model="itemInfo.warehouse_id" placeholder="请选择" clearable filterable>
<el-option v-for="it in warehouseList" :key="it.id" :label="it.name" :value="it.id" />
</el-select>
</el-form-item>
<el-form-item label="采购人:">
<el-input v-model="itemInfo.purchasing_agent" clearable></el-input>
</el-form-item>
<el-form-item label="入仓时间:">
<el-date-picker
v-model="itemInfo.in_put_time"
type="datetime"
format="YYYY-MM-DD HH:mm:ss"
value-format="YYYY-MM-DD HH:mm:ss"
placeholder="选择日期"
clearable>
</el-date-picker>
</el-form-item>
<el-form-item label="批次号:">
<el-input v-model="itemInfo.batch_number" clearable></el-input>
</el-form-item>
<el-form-item label="采购单:">
<el-upload
ref="uploadRef"
action="/api/upload/img"
:headers="headers"
:on-remove="handleImgRemove"
:on-error="handleUploadError"
:on-preview="handlePreview"
:on-success="handleImgSuccess"
:file-list="imgsList"
:show-file-list="true"
list-type="picture-card"
accept=".png,.jpg,.gif">
<el-icon><Plus /></el-icon>
</el-upload>
</el-form-item>
<el-form-item label="附件:">
<el-upload
action="/api/upload/file"
:on-success="handleFileSuccess"
:on-remove="handleFileRemove"
:before-upload="handleBeforeUpload"
:on-error="handleUploadError"
:on-preview="previewFile"
:headers="headers"
:file-list="fileList"
:show-file-list="true">
<el-button type="primary">上传附件</el-button>
<template #tip>
<div class="el-upload__tip">请上传zippdfexceldocdocx格式附件最大限制为 4 M</div>
</template>
</el-upload>
</el-form-item>
<el-form-item label="备注:">
<el-input v-model="itemInfo.note" type="textarea" :rows="4"></el-input>
</el-form-item>
</el-form>
<div v-for="(item, i) in skusList" :key="i" class="skuBox">
<div class="tit">采购商品{{ i + 1 }}</div>
<el-form label-width="110px" :inline="true">
<!-- <el-form-item label="所属商品:">
<el-select v-model="item.goods_id" placeholder="请选择" clearable filterable style="width: 220px;" @change="changeGoods(item)">
<el-option v-for="it in allGoodsList" :key="it.id" :label="it.title" :value="it.id" />
</el-select>
</el-form-item> -->
<el-form-item label="商品/规格:" style="width: 100%;">
<el-select v-model="item.sku_id" placeholder="请选择" clearable filterable style="width: 680px;">
<el-option v-for="it in guigeList" :key="it.id" :label="it.goods && it.goods.title + ' - ' + it.title" :value="it.id" />
</el-select>
</el-form-item>
<el-form-item label="采购数量:">
<el-input-number v-model="item.num" :min="0" style="width: 270px;" />
</el-form-item>
<el-form-item label="采购成本:">
<el-input placeholder="采购成本" v-model="item.cost" clearable style="width: 270px;"></el-input>
</el-form-item>
<el-form-item label="生产日期:">
<el-date-picker
v-model="item.production_date"
type="date"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
placeholder="选择日期"
style="width: 270px;"
clearable>
</el-date-picker>
</el-form-item>
<el-form-item label="出厂日期:">
<el-date-picker
v-model="item.factory_date"
type="date"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
placeholder="选择日期"
style="width: 270px;"
clearable>
</el-date-picker>
</el-form-item>
<el-form-item label="有效期类型:">
<el-radio-group v-model="item.validity_type" style="width: 270px;">
<el-radio label="day"></el-radio>
<el-radio label="month"></el-radio>
<el-radio label="year"></el-radio>
<el-radio label="none">长期</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="有效时间:">
<el-input-number v-model="item.validity" :min="0" style="width: 270px;" />
</el-form-item>
<el-form-item label="批次号:">
<el-input placeholder="批次号" v-model="item.batch_number" clearable style="width: 270px;"></el-input>
</el-form-item>
<el-form-item label=" ">
<el-button type="danger" @click="handleSkuDelete(i)" size="small"><el-icon><Delete /></el-icon>&nbsp;删除</el-button>
</el-form-item>
</el-form>
</div>
<el-form label-position="right" label-width="110px">
<el-form-item label="">
<el-button type="success" @click="toAddSku()">增加采购商品</el-button>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="showDialog = false">取消</el-button>
<el-button type="primary" @click="commitOpa()" :loading="opa_loading">确定</el-button>
</span>
</template>
</el-dialog>
<!-- 预览图片 -->
<el-dialog v-model="picVisible" center width="800px">
<img :src="dialogImageUrl" style="max-width: 700px; margin: 0 auto;display: block;" />
</el-dialog>
</div>
</template>
<script>
import { onMounted, reactive, toRefs, ref, nextTick } from "vue"
import { get, post } from "@/api/request"
import { Search, Plus, Edit, ZoomIn, Delete } from '@element-plus/icons'
import { ElMessage, ElMessageBox } from 'element-plus'
import { parseErrors } from 'components/common'
export default {
components: {
Search, Plus, Edit, ZoomIn, Delete
},
setup() {
const data = reactive({
goodsList: [],
page: 1,
pageSize: 10,
total: 0,
loading: false,
pickerTime: [],
showDialog: false,
opa_loading: false,
headers: {
Authorization: localStorage.getItem('token'),
'Shop-Id': localStorage.getItem('shopId') || ''
},
picVisible: false,
dialogImageUrl: '',
imgsList: [],
fileList: [],
Type: {
'default': '默认'
},
warehouseList: [],
suppliersList: [],
guigeList: [],
itemInfo: {},
skusList: [],
qaFileList: [],
allGoodsList: [],
})
function handleSearch() {
data.page = 1
fetchData()
}
const fetchData = () => {
data.loading = true
let params = {
page: data.page,
pageSize: data.pageSize,
service_id: data.service_id,
start_date: data.pickerTime ? data.pickerTime[0] : '',
end_date: data.pickerTime ? data.pickerTime[1] : ''
}
get(`/api/purchases`, params).then((res) => {
data.goodsList = res.data
data.total = res.meta.total
data.loading = false
}).catch(() => {
data.loading = false
})
}
function handleCurrentChange(e) {
data.page = e
fetchData()
}
function handleSizeChange(e) {
data.page = 1
data.pageSize = e
fetchData()
}
function handleAdd() {
data.imgsList = []
data.qaFileList = []
data.skusList = []
data.itemInfo = {
type: 'default'
}
data.showDialog = true
}
function commitOpa() {
if(!data.skusList.length) {
ElMessage({ type: 'info', message: '请添加采购商品' })
return
}
data.opa_loading = true
let imgs = []
data.imgsList.forEach((it) => {
imgs.push(it.url)
})
let params = {
...data.itemInfo
}
params.goodsSkus = data.skusList
params.images = imgs
params.attachments = data.qaFileList.length ? data.qaFileList[0].url : ''
post(`/api/purchases`, params).then(() => {
data.page = 1
fetchData()
ElMessage({ type: 'success', message: '新增成功' })
data.showDialog = false
data.opa_loading = false
}).catch((err) => {
data.opa_loading = false
parseErrors(err.response.data.errors)
})
}
function handleImgRemove(res, ress) {
data.replyImg = ress
}
function handleUploadError(err) {
if(JSON.parse(err.message) && JSON.parse(err.message).message) {
ElMessage({ type: 'error', message: JSON.parse(err.message).message })
}
}
function handlePreview(File) {
data.dialogImageUrl = File.url
data.picVisible = true
}
function handleImgSuccess(res) {
data.imgsList.push({url: res.data.link})
}
function handleFileSuccess(res, ress) {
if (res.code === 0) {
data.qaFileList = [{ name: ress.name, url: res.data.link }]
}
}
function handleFileRemove(res, ress) {
data.qaFileList = ress
}
function previewFile(File) {
window.open(File.response.data.link)
}
const handleBeforeUpload = (file) => {
let type = file.name.indexOf('.pdf') !== -1 || file.name.indexOf('.doc') !== -1 || file.name.indexOf('.docx') !== -1 || file.name.indexOf('.zip') !== -1 || file.name.indexOf('.xlsx') !== -1
if (type) {
const isLt4M = file.size / 1024 / 1024 < 4
if (!isLt4M) {
ElMessage.error('附件上传大小不能超过 4MB!')
return false
}
} else {
ElMessage.error('上传文件格式不正确')
return false
}
}
function handleSkuDelete(index) {
data.skusList.splice(index, 1)
}
const uploadRef = ref(null)
const resetForm = () => {
data.imgsList = []
nextTick(() => {
uploadRef.value.clearFiles()
})
}
function toAddSku() {
let sku = {
goods_id: '',
sku_id: '',
num: 1,
cost: '',
production_date: '',
factory_date: '',
validity_type: '',
validity: '',
batch_number: ''
}
data.skusList.push(sku)
}
function getWarehouseList() {
get(`/api/all/warehouses`).then((res) => {
data.warehouseList = res.data
})
}
function getSuppliersList() {
get(`/api/suppliers`, {pageSize: 1000}).then((res) => {
data.suppliersList = res.data
})
}
function getSkusList() {
get(`/api/all/goods-skus`).then((res) => {
data.guigeList = res.data
})
}
function getAllGoodsList() {
get(`/api/all/goods`).then((res) => {
data.allGoodsList = res.data
})
}
function changeGoods(row) {
row.sku_id = ''
get(`/api/all/goods-skus`, {goods_id: row.goods_id}).then((res) => {
data.guigeList = res.data
})
}
onMounted(() => {
fetchData()
getWarehouseList()
getSuppliersList()
getSkusList()
getAllGoodsList()
})
return {
uploadRef,
...toRefs(data),
handleSearch,
handleCurrentChange,
handleSizeChange,
fetchData,
handleAdd,
commitOpa,
handleImgRemove,
handleUploadError,
handlePreview,
handleImgSuccess,
handleSkuDelete,
resetForm,
toAddSku,
handleFileSuccess,
handleFileRemove,
previewFile,
handleBeforeUpload,
getWarehouseList,
getSuppliersList,
getSkusList,
getAllGoodsList,
changeGoods
}
}
}
</script>
<style lang="scss" scoped>
.searchBox{
display: flex;
flex-wrap: wrap;
background-color: #fff;
padding: 15px 0 0 0;
border-radius: 4px;
margin-bottom: 15px;
.row{
display: flex;
align-items: center;
width: 20%;
box-sizing: border-box;
margin-bottom: 15px;
&.row1{
width: 25%;
}
.span{
display: block;
width: 80px;
font-size: 14px;
text-align: right;
box-sizing: border-box;
}
.right{
width: calc(100% - 100px);
}
.wid100{
width: 100%;
}
}
}
.opaBox{
margin-bottom: 15px;
}
.imgBox{
display: flex;
flex-wrap: wrap;
.el-image{
width: 60px;
height: 60px;
border-radius: 4px;
margin-right: 10px;
}
}
.skuBox{
border: 1px solid #e5e5e5;
border-radius: 5px;
padding: 15px 0;
margin-bottom: 15px;
background-color: #f3f3f3;
.tit{
padding-left: 40px;
font-weight: 600;
font-size: 15px;
margin-bottom: 15px;
}
}
</style>

View File

@ -0,0 +1,12 @@
<script>
export default {
created() {
const { params, query } = this.$route
const { path } = params
this.$router.replace({ path: '/' + path, query })
},
render: function(h) {
return h() // avoid warning message
}
}
</script>

276
src/views/shops/index.vue Normal file
View File

@ -0,0 +1,276 @@
<template>
<div class="pageBox">
<!-- <div class="searchBox">
<div class="row">
<span class="span">商品名称</span>
<div class="right"><el-input v-model="goods_name" class="wid100" clearable></el-input></div>
</div>
<div class="row">
<el-button type="primary" @click="handleSearch"><el-icon><Search /></el-icon>&nbsp;筛选</el-button>
</div>
</div> -->
<el-card shadow="never">
<div class="opaBox">
<el-button type="primary" @click="handleAdd"><el-icon><Plus /></el-icon>&nbsp;新增</el-button>
<!-- <el-button type="warning" @click="handleExport"><span class="iconfont icon-daochu"></span>&nbsp;导出</el-button> -->
</div>
<el-table :data="shopsList" style="width: 100%" border v-loading="loading">
<el-table-column prop="id" label="ID" width="80" align="center" />
<el-table-column prop="name" label="店铺名称" align="center" />
<el-table-column label="平台" align="center">
<template #default="scope">
<span>{{ Plat[scope.row.plat] }}</span>
</template>
</el-table-column>
<el-table-column prop="mx_supplier_name" label="供应商" align="center" />
<el-table-column label="ERP库存同步" align="center">
<template #default="scope">
<span>{{ scope.row.sync_stock ? '启用' : '不启用' }}</span>
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="150">
<template #default="scope">
<el-button type="primary" circle @click="handleEdit(scope.row)"><el-icon><Edit /></el-icon></el-button>
</template>
</el-table-column>
</el-table>
<div class="page-pagination">
<el-pagination
:current-page="page"
background
layout="prev, pager, next, sizes, total"
:total="total"
:page-sizes="[10, 50, 100]"
:page-size="pageSize"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"></el-pagination>
</div>
</el-card>
<el-dialog v-model="showDialog" width="900px" :title="opaType == 'add' ? '新增' : '编辑' ">
<el-form label-position="right" label-width="110px">
<el-form-item label="店铺名称:">
<el-input v-model="itemInfo.name" clearable></el-input>
</el-form-item>
<el-form-item label="平台:">
<el-radio-group v-model="itemInfo.plat">
<el-radio label="MX">妙选</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="供应商:">
<el-select v-model="itemInfo.mx_supplier_id" placeholder="请选择" clearable filterable>
<el-option v-for="it in suppliersList" :key="it.id" :label="it.name" :value="it.id" />
</el-select>
</el-form-item>
<el-form-item label="是否同步库存:">
<el-radio-group v-model="itemInfo.sync_stock">
<el-radio :label="1"></el-radio>
<el-radio :label="0"></el-radio>
</el-radio-group>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="showDialog = false">取消</el-button>
<el-button type="primary" @click="commitOpa()" :loading="opa_loading">确定</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script>
import { onMounted, reactive, toRefs, ref, nextTick } from "vue"
import { get, post } from "@/api/request"
import { Search, Plus, Edit, ZoomIn, Delete } from '@element-plus/icons'
import { ElMessage, ElMessageBox } from 'element-plus'
import { parseErrors } from 'components/common'
export default {
components: {
Search, Plus, Edit, ZoomIn, Delete
},
setup() {
const data = reactive({
shopsList: [],
page: 1,
pageSize: 10,
total: 0,
loading: false,
pickerTime: [],
opaType: '',
showDialog: false,
opa_loading: false,
Plat: {
'MX': '妙选'
},
suppliersList: [],
itemInfo: {}
})
function handleSearch() {
data.page = 1
fetchData()
}
const fetchData = () => {
data.loading = true
let params = {
page: data.page,
pageSize: data.pageSize,
service_id: data.service_id,
start_date: data.pickerTime ? data.pickerTime[0] : '',
end_date: data.pickerTime ? data.pickerTime[1] : ''
}
get(`/api/shops`, params).then((res) => {
data.shopsList = res.data
data.total = res.meta.total
data.loading = false
}).catch(() => {
data.loading = false
})
}
function handleCurrentChange(e) {
data.page = e
fetchData()
}
function handleSizeChange(e) {
data.page = 1
data.pageSize = e
fetchData()
}
function handleAdd() {
data.opaType = 'add'
data.itemInfo = {
plat: 'MX',
sync_stock: 1
}
data.showDialog = true
}
function handleEdit(item) {
data.opaType = 'edit'
data.itemInfo = JSON.parse(JSON.stringify(item))
data.showDialog = true
}
function commitOpa() {
data.opa_loading = true
let params = {
...data.itemInfo
}
if(data.opaType == 'add') {
post(`/api/shops`, params).then(() => {
data.page = 1
fetchData()
ElMessage({ type: 'success', message: '新增成功' })
data.showDialog = false
data.opa_loading = false
}).catch((err) => {
data.opa_loading = false
parseErrors(err.response.data.errors)
})
} else {
post(`/api/shops/${data.itemInfo.id}`, params, 'PUT').then(() => {
fetchData()
ElMessage({ type: 'success', message: '编辑成功' })
data.showDialog = false
data.opa_loading = false
}).catch((err) => {
data.opa_loading = false
parseErrors(err.response.data.errors)
})
}
}
function getSuppliersList() {
get(`/api/mxSuppliers`, {pageSize: 1000}).then((res) => {
data.suppliersList = res.data
})
}
onMounted(() => {
fetchData()
getSuppliersList()
})
return {
...toRefs(data),
handleSearch,
handleCurrentChange,
handleSizeChange,
fetchData,
handleAdd,
handleEdit,
commitOpa,
getSuppliersList
}
}
}
</script>
<style lang="scss" scoped>
.searchBox{
display: flex;
flex-wrap: wrap;
background-color: #fff;
padding: 15px 0 0 0;
border-radius: 4px;
margin-bottom: 15px;
.row{
display: flex;
align-items: center;
width: 20%;
box-sizing: border-box;
margin-bottom: 15px;
&.row1{
width: 25%;
}
.span{
display: block;
width: 80px;
font-size: 14px;
text-align: right;
box-sizing: border-box;
}
.right{
width: calc(100% - 100px);
}
.wid100{
width: 100%;
}
}
}
.opaBox{
margin-bottom: 15px;
}
.imgBox{
display: flex;
flex-wrap: wrap;
.el-image{
width: 60px;
height: 60px;
border-radius: 4px;
margin-right: 10px;
}
}
.skuBox{
border: 1px solid #e5e5e5;
border-radius: 5px;
padding: 15px 0;
margin-bottom: 15px;
background-color: #f3f3f3;
.tit{
padding-left: 40px;
font-weight: 600;
font-size: 15px;
margin-bottom: 15px;
}
}
</style>

View File

@ -0,0 +1,639 @@
<template>
<div class="pageBox">
<div class="searchBox">
<div class="row row1">
<span class="span">商品名称</span>
<div class="right"><el-input v-model="filter.title" class="wid100" clearable></el-input></div>
</div>
<div class="row row1">
<span class="span">规格编码</span>
<div class="right"><el-input v-model="filter.sku_code" class="wid100" clearable></el-input></div>
</div>
<div class="row row1">
<span class="span">商品品牌</span>
<div class="right">
<el-select v-model="filter.brand_id" placeholder="请选择" clearable class="wid100">
<el-option v-for="it in brandList" :key="it.id" :label="it.name" :value="it.id" />
</el-select>
</div>
</div>
<div class="row row1">
<span class="span">赠品</span>
<div class="right">
<el-select v-model="filter.gift" placeholder="请选择" :clearable="false" class="wid100">
<el-option label="全部" value="all" />
<el-option label="否" :value="0" />
<el-option label="是" :value="1" />
</el-select>
</div>
</div>
<div class="row">
<span class="span">时间区间</span>
<div class="right">
<el-date-picker
v-model="rangeTime"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="YYYY-MM-DD"
format="YYYY-MM-DD"
style="width: 250px;">
</el-date-picker>
</div>
</div>
<div class="row">
<span class="span"></span>
<el-button type="primary" @click="handleSearch"><el-icon><Search /></el-icon>&nbsp;筛选</el-button>
</div>
</div>
<el-card shadow="never">
<el-table :data="statisticsList" style="width: 100%" border v-loading="loading" @sort-change="sortChange"
:default-sort="{
prop: 'number,order_num,seven_day_avg_number,stock_wait,three_day_avg_number,three_day_stock_wait,thirty_day_number,actual_inventory,total_profit',
}">
<el-table-column prop="date" label="日期" align="center" />
<el-table-column prop="type" label="商品信息" width="220">
<template #default="scope">
<div class="goodInfo" v-if="scope.row.goods_sku">
<div class="imgBox" v-if="scope.row.goods_sku.goods && scope.row.goods_sku.goods.images">
<el-image v-for="(it, i) in scope.row.goods_sku.goods.images" :key="i" :z-index="9999"
:src="it" :hide-on-click-modal="true" :preview-src-list="[scope.row.goods_sku.goods.images]"
fit="cover" :preview-teleported="true" />
</div>
<div class="tit">{{ scope.row.goods_sku.goods.title }}({{ scope.row.goods_sku.title }})</div>
</div>
</template>
</el-table-column>
<el-table-column prop="number" label="销量" align="center" sortable="custom" />
<el-table-column prop="order_num" label="单数" align="center" sortable="custom" />
<el-table-column prop="seven_day_avg_number" label="7天日销" align="center" sortable="custom" />
<el-table-column prop="stock_wait" label="7天周转天数" align="center" sortable="custom" />
<el-table-column prop="three_day_avg_number" label="3天日销" align="center" sortable="custom" />
<el-table-column prop="three_day_stock_wait" label="3天周转天数" align="center" sortable="custom" />
<el-table-column prop="thirty_day_number" label="近30天销量" align="center" sortable="custom" />
<el-table-column prop="actual_inventory" label="总库存" align="center" sortable="custom">
<template #header>
<span style="margin-right: 5px;">总库存</span>
<span>
<el-tooltip placement="top" :hide-after="0" :show-after="200">
<template #content>总库存包含未锁定和锁定库存括号里为锁定库存</template>
<el-icon size="18"><QuestionFilled /></el-icon>
</el-tooltip>
</span>
</template>
<template #default="scope">
<div v-if="scope.row.goods_sku">{{ scope.row.goods_sku.actual_inventory }}{{ scope.row.goods_sku.lock_in_stock }}</div>
</template>
</el-table-column>
<el-table-column prop="refund_amount" label="退款金额" align="center" />
<el-table-column prop="red_refund_amount" label="红包退款有责金额" align="center" />
<el-table-column prop="total_profit" label="总利润" align="center" />
<el-table-column label="数据" align="center" width="180">
<template #default="scope">
<el-button type="primary" circle @click="handleAnalysis(scope.row.sku_code)" title="店铺数据"><el-icon><Shop /></el-icon></el-button>
<el-button type="primary" circle :loading="scope.row.loading" @click="trendCharts(scope.row)" title="趋势图"><el-icon><TrendCharts /></el-icon></el-button>
<el-button type="primary" circle @click="orderCharts(scope.row)" title="订单商品销量趋势"><el-icon><List /></el-icon></el-button>
</template>
</el-table-column>
</el-table>
<div class="page-pagination">
<el-pagination
:current-page="page"
background
layout="prev, pager, next, sizes, total"
:total="total"
:page-sizes="[10, 50, 100]"
:page-size="pageSize"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"></el-pagination>
</div>
</el-card>
<el-dialog v-model="showDialog" width="1000px" title="店铺数据">
<div>
<div style="margin-bottom: 15px;">
<span>时间</span>
<el-date-picker
v-model="pickTime"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="YYYY-MM-DD"
format="YYYY-MM-DD"
style="width: 250px;"
:clearable="false"
@change="getDialogList()">
</el-date-picker>
</div>
<div class="tabBox">
<el-table :data="dialogList" style="width: 100%" border v-loading="opa_loading">
<el-table-column label="排名" width="70" align="center">
<template #default="scope">
<span>{{ scope.$index + 1 }}</span>
</template>
</el-table-column>
<el-table-column prop="mx_shop_name" label="店铺" align="center" />
<el-table-column prop="date" label="日期" align="center" />
<el-table-column prop="number" label="销量" align="center" />
<el-table-column prop="order_num" label="单数" align="center" />
<el-table-column prop="avg_number" label="日销量" align="center" />
<el-table-column prop="total_price" label="支付金额" align="center" />
<el-table-column prop="refund_amount" label="退款金额" align="center" />
<el-table-column prop="total_profit" label="利润" align="center" />
<el-table-column label="趋势图" align="center" width="80">
<template #default="scope">
<el-button type="primary" circle @click="getDataLine(scope.row)" :loading="scope.row.loading"><el-icon><TrendCharts /></el-icon></el-button>
</template>
</el-table-column>
</el-table>
</div>
</div>
</el-dialog>
<el-dialog v-model="showChart" width="900px" title="趋势图分析">
<el-form label-width="110px">
<el-form-item label="时间:">
<div>
<el-date-picker
v-model="chartTime"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="YYYY-MM-DD"
format="YYYY-MM-DD"
style="width: 250px;"
:clearable="false"
@change="changeTime0()">
</el-date-picker>
</div>
</el-form-item>
</el-form>
<div id="lineChart" style="width: 100%;height:500px;" v-loading="loading1"></div>
</el-dialog>
<el-dialog v-model="showTrend" width="900px" title="趋势图分析">
<el-form label-width="110px" :inline="true">
<el-form-item label="时间:">
<div>
<el-date-picker
v-model="trendTime"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="YYYY-MM-DD"
format="YYYY-MM-DD"
style="width: 250px;"
:clearable="false"
@change="changeTime()">
</el-date-picker>
</div>
</el-form-item>
<el-form-item label="总计:"><span style="color: #f00;">{{ sumDaily }}</span></el-form-item>
</el-form>
<div id="trendChart" style="width: 100%;height:500px;" v-loading="loading1"></div>
</el-dialog>
<el-dialog v-model="showOrderTrend" width="900px" title="订单商品销量趋势">
<el-form label-width="80px" :inline="true">
<el-form-item label="店铺:">
<el-select v-model="from_shop_ids" @change="changeOrderTime()" placeholder="请选择" clearable filterable multiple collapse-tags style="width: 200px;">
<el-option v-for="it in shopsList" :key="it.id" :label="it.name" :value="it.id" />
</el-select>
</el-form-item>
<el-form-item label="时间:">
<div>
<el-date-picker
v-model="orderTrendTime"
type="datetimerange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="YYYY-MM-DD HH:mm:ss"
format="YYYY-MM-DD HH:mm:ss"
style="width: 340px;"
:clearable="false"
@change="changeOrderTime()">
</el-date-picker>
</div>
</el-form-item>
<el-form-item label="粒度:">
<el-radio-group v-model="grain_size" size="mini" @change="changeOrderTime()">
<el-radio-button label="60分钟" :value="60" />
<el-radio-button label="30分钟" :value="30" />
<el-radio-button label="15分钟" :value="15" />
</el-radio-group>
</el-form-item>
</el-form>
<div id="orderChart" style="width: 100%;height:500px;" v-loading="loading1"></div>
</el-dialog>
</div>
</template>
<script>
import { onMounted, reactive, toRefs } from "vue"
import { get } from "@/api/request"
import { Search, Shop, TrendCharts, QuestionFilled, List } from '@element-plus/icons'
import * as echarts from 'echarts'
import dayjs from 'dayjs'
export default {
components: {
Search, Shop, TrendCharts, QuestionFilled, List
},
setup() {
const data = reactive({
filter: {
title: '',
gift: 0
},
rangeTime: [],
statisticsList: [],
page: 1,
pageSize: 10,
total: 0,
loading: false,
opaType: '',
showDialog: false,
opa_loading: false,
itemId: 0,
from_shop_id: 0,
dialogList: [],
pickTime: [],
loading1: false,
dataLineX: [],
dataLineY: [],
showChart: false,
brandList: [],
ascOrDesc: 'desc',
sort: 'number',
showTrend: false,
sumDaily: 0,
trendTime: [],
chartTime: [],
showOrderTrend: false,
orderTrendTime: [],
from_shop_ids: [],
shopsList: [],
grain_size: 60,
dataLineY2: []
})
function handleSearch() {
data.page = 1
fetchData()
}
function getShopsList() {
get(`/api/mxShops`).then((res) => {
data.shopsList = res.data
})
}
const fetchData = () => {
data.loading = true
let params = {
...data.filter,
page: data.page,
pageSize: data.pageSize,
start_date: data.rangeTime ? data.rangeTime[0] : '',
end_date: data.rangeTime ? data.rangeTime[1] : '',
direction: data.ascOrDesc,
order_by_column: data.sort
}
params.brand_id = data.filter.brand_id || 0
get(`/api/orderItemDailyReport`, params).then((res) => {
data.statisticsList = res.data
data.total = res.meta.total
data.loading = false
}).catch(() => {
data.loading = false
})
}
function handleCurrentChange(e) {
data.page = e
fetchData()
}
function handleSizeChange(e) {
data.page = 1
data.pageSize = e
fetchData()
}
function handleAnalysis(sku_code) {
let end = dayjs().format('YYYY-MM-DD')
let start = dayjs().subtract(30, 'day').format('YYYY-MM-DD')
data.pickTime = [end, end]
data.itemId = sku_code
getDialogList()
data.showDialog = true
}
const getDialogList = () => {
data.opa_loading = true
let params = {
sku_code: data.itemId,
start_date: data.pickTime ? data.pickTime[0] : '',
end_date: data.pickTime ? data.pickTime[1] : ''
}
get(`/api/mxShopDailyReport`, params).then((res) => {
data.dialogList = res.data
data.opa_loading = false
}).catch(() => {
data.opa_loading = false
})
}
const getDataLine = (row) => {
row.loading = true
data.loading1 = true
let end = dayjs().format('YYYY-MM-DD')
let start = dayjs().subtract(30, 'day').format('YYYY-MM-DD')
data.chartTime = [start, end]
data.from_shop_id = row.from_shop_id
let params = {
from_shop_id: row.from_shop_id,
sku_code: data.itemId,
start_date: data.chartTime ? data.chartTime[0] : '',
end_date: data.chartTime ? data.chartTime[1] : ''
}
get(`/api/mxShopTendency`, params).then((res) => {
data.dataLineX = []
data.dataLineY = []
res.data.forEach((item) => {
data.dataLineX.push(item.date)
data.dataLineY.push(item.number)
})
data.showChart = true
setTimeout(() => {
getTrendChart('lineChart')
data.loading1 = false
}, 500)
row.loading = false
}).catch(() => {
row.loading = false
})
}
function changeTime0() {
data.loading1 = true
let params = {
from_shop_id: data.from_shop_id,
sku_code: data.itemId,
start_date: data.chartTime ? data.chartTime[0] : '',
end_date: data.chartTime ? data.chartTime[1] : ''
}
get(`/api/mxShopTendency`, params).then((res) => {
data.dataLineX = []
data.dataLineY = []
res.data.forEach((item) => {
data.dataLineX.push(item.date)
data.dataLineY.push(item.number)
})
setTimeout(() => {
getTrendChart('lineChart')
data.loading1 = false
}, 500)
}).catch(() => {
data.loading = false
})
}
function getTrendChart(id, val = 0) {
let myChart = echarts.init(document.getElementById(id))
let option = {
tooltip: {
trigger: 'axis'
},
legend: {
data: ['销量']
},
xAxis: {
type: 'category',
data: data.dataLineX
},
yAxis: {},
series: [
{
name: '销量',
type: 'line',
smooth: true,
data: data.dataLineY
}
]
}
if(val == 2) {
option.legend.data.push('累计销量')
option.series.push({
name: '累计销量',
type: 'line',
smooth: true,
data: data.dataLineY2
})
}
myChart.setOption(option)
}
function trendCharts(row) {
data.itemId = row.sku_code
row.loading = true
data.loading1 = true
let end = dayjs().format('YYYY-MM-DD')
let start = dayjs().subtract(30, 'day').format('YYYY-MM-DD')
data.trendTime = [start, end]
let params = {
sku_code: row.sku_code,
start_date: data.trendTime ? data.trendTime[0] : '',
end_date: data.trendTime ? data.trendTime[1] : ''
}
get(`/api/orderItemDailyTendency`, params).then((res) => {
data.dataLineX = []
data.dataLineY = []
res.data.forEach((item) => {
data.dataLineX.push(item.date)
data.dataLineY.push(item.number)
})
data.sumDaily = res.sum
data.showTrend = true
setTimeout(() => {
getTrendChart('trendChart')
data.loading1 = false
}, 500)
row.loading = false
}).catch(() => {
row.loading = false
})
}
function changeTime() {
data.loading1 = true
let params = {
sku_code: data.itemId,
start_date: data.trendTime ? data.trendTime[0] : '',
end_date: data.trendTime ? data.trendTime[1] : ''
}
get(`/api/orderItemDailyTendency`, params).then((res) => {
data.dataLineX = []
data.dataLineY = []
res.data.forEach((item) => {
data.dataLineX.push(item.date)
data.dataLineY.push(item.number)
})
data.sumDaily = res.sum
setTimeout(() => {
getTrendChart('trendChart')
data.loading1 = false
}, 500)
data.loading1 = false
}).catch(() => {
data.loading1 = false
})
}
function getBrandList() {
get(`/api/all/brands`).then((res) => {
data.brandList = res.data
})
}
function sortChange({ prop, order }) {
console.log(prop, order)
let arr = prop.split('.')
let length = arr.length
data.ascOrDesc = order == 'ascending' ? 'asc' : 'desc'
data.sort = arr[length - 1]
fetchData()
}
function orderCharts(row) {
data.from_shop_ids = []
data.itemId = row.sku_code
data.grain_size = 60
let end = dayjs().format('YYYY-MM-DD HH:mm:ss')
let start = dayjs().format('YYYY-MM-DD') + ' 00:00:00'
data.orderTrendTime = [start, end]
data.showOrderTrend = true
getOrderCharts()
}
function changeOrderTime() {
getOrderCharts()
}
function getOrderCharts() {
data.loading1 = true
let params = {
sku_code: data.itemId,
from_shop_ids: data.from_shop_ids,
start_time: data.orderTrendTime ? data.orderTrendTime[0] : '',
end_time: data.orderTrendTime ? data.orderTrendTime[1] : '',
minute: data.grain_size
}
get(`/api/orderItemTendency`, params).then((res) => {
data.dataLineX = []
data.dataLineY = []
data.dataLineY2 = []
res.data.forEach((item) => {
data.dataLineX.push(item.time_period)
data.dataLineY.push(item.total_number)
data.dataLineY2.push(item.total)
})
setTimeout(() => {
getTrendChart('orderChart', 2)
data.loading1 = false
}, 500)
data.loading1 = false
}).catch(() => {
data.loading1 = false
})
}
onMounted(() => {
fetchData()
getBrandList()
getShopsList()
})
return {
...toRefs(data),
handleSearch,
handleCurrentChange,
handleSizeChange,
fetchData,
handleAnalysis,
getDataLine,
getBrandList,
getDialogList,
sortChange,
trendCharts,
changeTime,
changeTime0,
orderCharts,
getOrderCharts,
changeOrderTime,
getShopsList
}
}
}
</script>
<style lang="scss" scoped>
.searchBox{
display: flex;
flex-wrap: wrap;
background-color: #fff;
padding: 15px 0 0 0;
border-radius: 4px;
margin-bottom: 15px;
.row{
display: flex;
align-items: center;
width: auto;
box-sizing: border-box;
margin-bottom: 15px;
margin-right: 15px;
&.row1{
width: 300px;
}
.span{
display: block;
width: 80px;
font-size: 14px;
text-align: right;
box-sizing: border-box;
}
.right{
width: calc(100% - 80px);
}
}
}
.imgBox{
.el-image{
width: 60px;
height: 60px;
border-radius: 4px;
margin-right: 10px;
display: inline-block;
}
}
.skuBox{
border: 1px solid #e5e5e5;
border-radius: 5px;
padding: 15px 0;
margin-bottom: 15px;
background-color: #f3f3f3;
.tit{
padding-left: 40px;
font-weight: 600;
font-size: 15px;
margin-bottom: 15px;
}
}
</style>

View File

@ -0,0 +1,272 @@
<template>
<div class="pageBox">
<!-- <div class="searchBox">
<div class="row">
<span class="span">商品名称</span>
<div class="right"><el-input v-model="goods_name" class="wid100" clearable></el-input></div>
</div>
<div class="row">
<el-button type="primary" @click="handleSearch"><el-icon><Search /></el-icon>&nbsp;筛选</el-button>
</div>
</div> -->
<el-card shadow="never">
<div class="opaBox">
<el-button type="primary" @click="handleAdd"><el-icon><Plus /></el-icon>&nbsp;新增</el-button>
<!-- <el-button type="warning" @click="handleExport"><span class="iconfont icon-daochu"></span>&nbsp;导出</el-button> -->
</div>
<el-table :data="goodsList" style="width: 100%" border v-loading="loading">
<el-table-column prop="id" label="ID" width="80" align="center" />
<el-table-column prop="name" label="供应商名称" align="center" />
<el-table-column prop="type" label="供应商类型" align="center">
<template #default="scope">
<span>{{ Type[scope.row.type] }}</span>
</template>
</el-table-column>
<el-table-column label="状态" align="center">
<template #default="scope">
<span>{{ scope.row.status ? '启用' : '不启用' }}</span>
</template>
</el-table-column>
<el-table-column prop="admin_user.username" label="添加人" align="center" />
<el-table-column prop="sort" label="排序" align="center" />
<el-table-column label="操作" align="center" width="150">
<template #default="scope">
<el-button type="primary" circle @click="handleEdit(scope.row)"><el-icon><Edit /></el-icon></el-button>
</template>
</el-table-column>
</el-table>
<div class="page-pagination">
<el-pagination
:current-page="page"
background
layout="prev, pager, next, sizes, total"
:total="total"
:page-sizes="[10, 50, 100]"
:page-size="pageSize"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"></el-pagination>
</div>
</el-card>
<el-dialog v-model="showDialog" width="900px" :title="opaType == 'add' ? '新增' : '编辑' ">
<el-form label-position="right" label-width="110px">
<el-form-item label="供应商名称:">
<el-input v-model="itemInfo.name" clearable></el-input>
</el-form-item>
<el-form-item label="供应商类型:">
<el-radio-group v-model="itemInfo.type">
<el-radio label="default">默认</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="状态:">
<el-radio-group v-model="itemInfo.status">
<el-radio :label="1">启用</el-radio>
<el-radio :label="0">不启用</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="备注:">
<el-input v-model="itemInfo.note" type="textarea" :rows="4"></el-input>
</el-form-item>
<el-form-item label="排序:">
<el-input-number v-model="itemInfo.sort" :min="0" />
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="showDialog = false">取消</el-button>
<el-button type="primary" @click="commitOpa()" :loading="opa_loading">确定</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script>
import { onMounted, reactive, toRefs, ref, nextTick } from "vue"
import { get, post } from "@/api/request"
import { Search, Plus, Edit, ZoomIn, Delete } from '@element-plus/icons'
import { ElMessage, ElMessageBox } from 'element-plus'
import { parseErrors } from 'components/common'
export default {
components: {
Search, Plus, Edit, ZoomIn, Delete
},
setup() {
const data = reactive({
goodsList: [],
page: 1,
pageSize: 10,
total: 0,
loading: false,
pickerTime: [],
opaType: '',
showDialog: false,
opa_loading: false,
Type: {
'default': '默认'
},
warehouseList: [],
itemInfo: {}
})
function handleSearch() {
data.page = 1
fetchData()
}
const fetchData = () => {
data.loading = true
let params = {
page: data.page,
pageSize: data.pageSize,
service_id: data.service_id,
start_date: data.pickerTime ? data.pickerTime[0] : '',
end_date: data.pickerTime ? data.pickerTime[1] : ''
}
get(`/api/suppliers`, params).then((res) => {
data.goodsList = res.data
data.total = res.meta.total
data.loading = false
}).catch(() => {
data.loading = false
})
}
function handleCurrentChange(e) {
data.page = e
fetchData()
}
function handleSizeChange(e) {
data.page = 1
data.pageSize = e
fetchData()
}
function handleAdd() {
data.opaType = 'add'
data.itemInfo = {
type: 'default',
sort: 0,
status: 1
}
data.showDialog = true
}
function handleEdit(item) {
data.opaType = 'edit'
data.itemInfo = JSON.parse(JSON.stringify(item))
data.showDialog = true
}
function commitOpa() {
data.opa_loading = true
let params = {
...data.itemInfo
}
if(data.opaType == 'add') {
post(`/api/suppliers`, params).then(() => {
data.page = 1
fetchData()
ElMessage({ type: 'success', message: '新增成功' })
data.showDialog = false
data.opa_loading = false
}).catch((err) => {
data.opa_loading = false
parseErrors(err.response.data.errors)
})
} else {
post(`/api/suppliers/${data.itemInfo.id}`, params, 'PUT').then(() => {
fetchData()
ElMessage({ type: 'success', message: '编辑成功' })
data.showDialog = false
data.opa_loading = false
}).catch((err) => {
data.opa_loading = false
parseErrors(err.response.data.errors)
})
}
}
onMounted(() => {
fetchData()
})
return {
...toRefs(data),
handleSearch,
handleCurrentChange,
handleSizeChange,
fetchData,
handleAdd,
handleEdit,
commitOpa,
}
}
}
</script>
<style lang="scss" scoped>
.searchBox{
display: flex;
flex-wrap: wrap;
background-color: #fff;
padding: 15px 0 0 0;
border-radius: 4px;
margin-bottom: 15px;
.row{
display: flex;
align-items: center;
width: 20%;
box-sizing: border-box;
margin-bottom: 15px;
&.row1{
width: 25%;
}
.span{
display: block;
width: 80px;
font-size: 14px;
text-align: right;
box-sizing: border-box;
}
.right{
width: calc(100% - 100px);
}
.wid100{
width: 100%;
}
}
}
.opaBox{
margin-bottom: 15px;
}
.imgBox{
display: flex;
flex-wrap: wrap;
.el-image{
width: 60px;
height: 60px;
border-radius: 4px;
margin-right: 10px;
}
}
.skuBox{
border: 1px solid #e5e5e5;
border-radius: 5px;
padding: 15px 0;
margin-bottom: 15px;
background-color: #f3f3f3;
.tit{
padding-left: 40px;
font-weight: 600;
font-size: 15px;
margin-bottom: 15px;
}
}
</style>

View File

@ -0,0 +1,274 @@
<template>
<div class="pageBox">
<!-- <div class="searchBox">
<div class="row">
<span class="span">商品名称</span>
<div class="right"><el-input v-model="goods_name" class="wid100" clearable></el-input></div>
</div>
<div class="row">
<el-button type="primary" @click="handleSearch"><el-icon><Search /></el-icon>&nbsp;筛选</el-button>
</div>
</div> -->
<el-card shadow="never">
<div class="opaBox">
<el-button type="primary" @click="handleAdd"><el-icon><Plus /></el-icon>&nbsp;新增</el-button>
<!-- <el-button type="warning" @click="handleExport"><span class="iconfont icon-daochu"></span>&nbsp;导出</el-button> -->
</div>
<el-table :data="goodsList" style="width: 100%" border v-loading="loading">
<el-table-column prop="id" label="ID" width="80" align="center" />
<el-table-column prop="name" label="仓库名称" align="center" />
<el-table-column prop="type" label="仓库类型" align="center">
<template #default="scope">
<span>{{ Type[scope.row.type] }}</span>
</template>
</el-table-column>
<el-table-column label="仓库状态" align="center">
<template #default="scope">
<span>{{ scope.row.status ? '启用' : '不启用' }}</span>
</template>
</el-table-column>
<el-table-column prop="admin_user.username" label="添加人" align="center" />
<el-table-column prop="note" label="备注" align="center" />
<el-table-column prop="sort" label="排序" align="center" />
<el-table-column label="操作" align="center" width="150">
<template #default="scope">
<el-button type="primary" circle @click="handleEdit(scope.row)"><el-icon><Edit /></el-icon></el-button>
</template>
</el-table-column>
</el-table>
<div class="page-pagination">
<el-pagination
:current-page="page"
background
layout="prev, pager, next, sizes, total"
:total="total"
:page-sizes="[10, 50, 100]"
:page-size="pageSize"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"></el-pagination>
</div>
</el-card>
<el-dialog v-model="showDialog" width="900px" :title="opaType == 'add' ? '新增' : '编辑' ">
<el-form label-position="right" label-width="110px">
<el-form-item label="仓库名称:">
<el-input v-model="itemInfo.name" clearable></el-input>
</el-form-item>
<el-form-item label="仓库类型:">
<el-radio-group v-model="itemInfo.type">
<el-radio label="cloud">云仓</el-radio>
<el-radio label="local">本地仓</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="仓库状态:">
<el-radio-group v-model="itemInfo.status">
<el-radio :label="1">启用</el-radio>
<el-radio :label="0">不启用</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="备注:">
<el-input v-model="itemInfo.note" type="textarea" :rows="4"></el-input>
</el-form-item>
<el-form-item label="排序:">
<el-input-number v-model="itemInfo.sort" :min="0" />
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="showDialog = false">取消</el-button>
<el-button type="primary" @click="commitOpa()" :loading="opa_loading">确定</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script>
import { onMounted, reactive, toRefs, ref, nextTick } from "vue"
import { get, post } from "@/api/request"
import { Search, Plus, Edit, ZoomIn, Delete } from '@element-plus/icons'
import { ElMessage, ElMessageBox } from 'element-plus'
import { parseErrors } from 'components/common'
export default {
components: {
Search, Plus, Edit, ZoomIn, Delete
},
setup() {
const data = reactive({
goodsList: [],
page: 1,
pageSize: 10,
total: 0,
loading: false,
pickerTime: [],
opaType: '',
showDialog: false,
opa_loading: false,
Type: {
'cloud': '云仓',
'local': '本地仓'
},
itemInfo: {}
})
function handleSearch() {
data.page = 1
fetchData()
}
const fetchData = () => {
data.loading = true
let params = {
page: data.page,
pageSize: data.pageSize,
service_id: data.service_id,
start_date: data.pickerTime ? data.pickerTime[0] : '',
end_date: data.pickerTime ? data.pickerTime[1] : ''
}
get(`/api/warehouses`, params).then((res) => {
data.goodsList = res.data
data.total = res.meta.total
data.loading = false
}).catch(() => {
data.loading = false
})
}
function handleCurrentChange(e) {
data.page = e
fetchData()
}
function handleSizeChange(e) {
data.page = 1
data.pageSize = e
fetchData()
}
function handleAdd() {
data.opaType = 'add'
data.itemInfo = {
status: 1,
sort: 0,
type: 'cloud'
}
data.showDialog = true
}
function handleEdit(item) {
data.opaType = 'edit'
data.itemInfo = JSON.parse(JSON.stringify(item))
data.showDialog = true
}
function commitOpa() {
data.opa_loading = true
let params = {
...data.itemInfo
}
if(data.opaType == 'add') {
post(`/api/warehouses`, params).then(() => {
data.page = 1
fetchData()
ElMessage({ type: 'success', message: '新增成功' })
data.showDialog = false
data.opa_loading = false
}).catch((err) => {
data.opa_loading = false
parseErrors(err.response.data.errors)
})
} else {
post(`/api/warehouses/${data.itemInfo.id}`, params, 'PUT').then(() => {
fetchData()
ElMessage({ type: 'success', message: '编辑成功' })
data.showDialog = false
data.opa_loading = false
}).catch((err) => {
data.opa_loading = false
parseErrors(err.response.data.errors)
})
}
}
onMounted(() => {
fetchData()
})
return {
...toRefs(data),
handleSearch,
handleCurrentChange,
handleSizeChange,
fetchData,
handleAdd,
handleEdit,
commitOpa,
}
}
}
</script>
<style lang="scss" scoped>
.searchBox{
display: flex;
flex-wrap: wrap;
background-color: #fff;
padding: 15px 0 0 0;
border-radius: 4px;
margin-bottom: 15px;
.row{
display: flex;
align-items: center;
width: 20%;
box-sizing: border-box;
margin-bottom: 15px;
&.row1{
width: 25%;
}
.span{
display: block;
width: 80px;
font-size: 14px;
text-align: right;
box-sizing: border-box;
}
.right{
width: calc(100% - 100px);
}
.wid100{
width: 100%;
}
}
}
.opaBox{
margin-bottom: 15px;
}
.imgBox{
display: flex;
flex-wrap: wrap;
.el-image{
width: 60px;
height: 60px;
border-radius: 4px;
margin-right: 10px;
}
}
.skuBox{
border: 1px solid #e5e5e5;
border-radius: 5px;
padding: 15px 0;
margin-bottom: 15px;
background-color: #f3f3f3;
.tit{
padding-left: 40px;
font-weight: 600;
font-size: 15px;
margin-bottom: 15px;
}
}
</style>

191
src/views/warehouse/io.vue Normal file
View File

@ -0,0 +1,191 @@
<template>
<div class="pageBox">
<!-- <div class="searchBox">
<div class="row">
<span class="span">商品名称</span>
<div class="right"><el-input v-model="goods_name" class="wid100" clearable></el-input></div>
</div>
<div class="row">
<el-button type="primary" @click="handleSearch"><el-icon><Search /></el-icon>&nbsp;筛选</el-button>
</div>
</div> -->
<el-card shadow="never">
<!-- <div class="opaBox">
<el-button type="warning" @click="handleExport"><span class="iconfont icon-daochu"></span>&nbsp;导出</el-button>
</div> -->
<el-table :data="goodsList" style="width: 100%" border v-loading="loading">
<el-table-column prop="id" label="ID" width="80" align="center" />
<el-table-column prop="target_type" label="来源" align="center" />
<el-table-column prop="goods.title" label="商品名称" align="center" />
<el-table-column prop="goods_id" label="商品编码" align="center" />
<el-table-column prop="sku.title" label="规格名称" align="center" />
<el-table-column prop="sku.sku_code" label="规格编码" align="center" />
<el-table-column prop="num" label="数量" align="center" />
<el-table-column prop="cost" label="成本" align="center" />
<el-table-column prop="production_date" label="生产日期" align="center" />
<el-table-column prop="factory_date" label="出厂日期" align="center" />
<el-table-column prop="validity" label="有效时间" align="center">
<template #default="scope">
<span>{{scope.row.validity_type == 'none' ? '-' : scope.row.validity}}</span>
</template>
</el-table-column>
<el-table-column prop="validity_type" label="有效期类型" align="center">
<template #default="scope">
<span>{{Validity[scope.row.validity_type]}}</span>
</template>
</el-table-column>
<el-table-column prop="validity_end_time" label="截止有效期" align="center" />
<el-table-column prop="batch_number" label="批次号" align="center" />
<el-table-column prop="created_at" label="操作时间" align="center" />
</el-table>
<div class="page-pagination">
<el-pagination
:current-page="page"
background
layout="prev, pager, next, sizes, total"
:total="total"
:page-sizes="[10, 50, 100]"
:page-size="pageSize"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"></el-pagination>
</div>
</el-card>
</div>
</template>
<script>
import { onMounted, reactive, toRefs } from "vue"
import { get, post } from "@/api/request"
import { Search, Plus, Edit, ZoomIn, Delete } from '@element-plus/icons'
import { ElMessage, ElMessageBox } from 'element-plus'
export default {
components: {
Search, Plus, Edit, ZoomIn, Delete
},
setup() {
const data = reactive({
goodsList: [],
page: 1,
pageSize: 10,
total: 0,
loading: false,
Validity: {
'day': '天',
'month': '月',
'year': '年',
'none': '长期'
}
})
function handleSearch() {
data.page = 1
fetchData()
}
const fetchData = () => {
data.loading = true
let params = {
page: data.page,
pageSize: data.pageSize,
service_id: data.service_id,
start_date: data.pickerTime ? data.pickerTime[0] : '',
end_date: data.pickerTime ? data.pickerTime[1] : ''
}
get(`/api/goods-put-records`, params).then((res) => {
data.goodsList = res.data
data.total = res.meta.total
data.loading = false
}).catch(() => {
data.loading = false
})
}
function handleCurrentChange(e) {
data.page = e
fetchData()
}
function handleSizeChange(e) {
data.page = 1
data.pageSize = e
fetchData()
}
onMounted(() => {
fetchData()
})
return {
...toRefs(data),
handleSearch,
handleCurrentChange,
handleSizeChange,
fetchData,
}
}
}
</script>
<style lang="scss" scoped>
.searchBox{
display: flex;
flex-wrap: wrap;
background-color: #fff;
padding: 15px 0 0 0;
border-radius: 4px;
margin-bottom: 15px;
.row{
display: flex;
align-items: center;
width: 20%;
box-sizing: border-box;
margin-bottom: 15px;
&.row1{
width: 25%;
}
.span{
display: block;
width: 80px;
font-size: 14px;
text-align: right;
box-sizing: border-box;
}
.right{
width: calc(100% - 100px);
}
.wid100{
width: 100%;
}
}
}
.opaBox{
margin-bottom: 15px;
}
.imgBox{
display: flex;
flex-wrap: wrap;
.el-image{
width: 60px;
height: 60px;
border-radius: 4px;
margin-right: 10px;
}
}
.skuBox{
border: 1px solid #e5e5e5;
border-radius: 5px;
padding: 15px 0;
margin-bottom: 15px;
background-color: #f3f3f3;
.tit{
padding-left: 40px;
font-weight: 600;
font-size: 15px;
margin-bottom: 15px;
}
}
</style>

63
vue.config.js Normal file
View File

@ -0,0 +1,63 @@
const webpack = require('webpack')
const path = require('path')
function resolve(dir) {
return path.join(__dirname, dir)
}
module.exports = {
lintOnSave: false,
publicPath: "./", //配置打包之后的相对路径
devServer: {
open: true, // npm run serve后自动打开页面
// host: 'localhost',
port: 8080, // 开发服务器运行端口号
overlay: {
warnings: false,
errors: true
},
proxy: { //配置代理
"/api": {
// target: "http://192.168.0.52:86", // 本地
// target: "http://192.168.99.5:8093", // 本地
// target: "http://43.138.23.240:82", // 本地//
// target: "http://192.168.99.72:81", // 本地//
target: 'http://warehouse.chutang66.com/',
changeOrigin: true, //开启代理
pathRewrite: { // 重命名
"^/api": "api"
}
}
}
},
chainWebpack: (config) => {
config.resolve.alias
.set('@', resolve('src'))
.set('api', resolve('src/api'))
.set('components', resolve('src/components'))
.set('views', resolve('src/views'))
config.plugin('provide').use(webpack.ProvidePlugin, [{
$: 'jquery',
jquery: 'jquery',
jQuery: 'jquery',
'window.jQuery': 'jquery'
}])
config.plugin('define').tap((definitions) => {
Object.assign(definitions[0], {
__VUE_OPTIONS_API__: 'true',
__VUE_PROD_DEVTOOLS__: 'false',
__VUE_PROD_HYDRATION_MISMATCH_DETAILS__: 'false'
})
return definitions
})
},
configureWebpack: () => {
var obj = {
externals: {
'./cptable': 'var cptable',
'../xlsx.js': 'var _XLSX'
}
};
return obj
},
}