This commit is contained in:
shahaibo
2024-04-02 16:15:22 +08:00
commit 67f5ce46ee
61 changed files with 30215 additions and 0 deletions

3
.env.production Normal file
View File

@@ -0,0 +1,3 @@
NODE_ENV = 'production'
VUE_APP_BASE_URL = 'https://www.aida.com.hk'

2
.env.test Normal file
View File

@@ -0,0 +1,2 @@
NODE_ENV = 'production'
VUE_APP_BASE_URL = 'http://18.167.251.121:5568'

72
.eslintrc.js Normal file
View File

@@ -0,0 +1,72 @@
module.exports = {
root: true, // 停止在父级目录中寻找
env: {
es6: true, // 启用 ES6 语法支持以及新的 ES6 全局变量或类型
node: true // Node.js 全局变量和 Node.js 作用域
},
extends: ['plugin:vue/essential'],
rules: {
'no-alert': 0, // 禁止使用alert confirm prompt
'no-console': 0, // 禁止使用console
'no-debugger': 0, // 禁止使用debugger
'prefer-const': 0, // 建议使用 const 关闭
'no-dupe-keys': 2, // 在创建对象字面量时不允许键重复 {a:1,a:1}
'no-dupe-args': 2, // 函数参数不能重复
'no-duplicate-imports': [
1,
{
includeExports: true
}
], // 不允许重复导入
'no-duplicate-case': 2, // switch中的case标签不能重复
'padded-blocks': 0, // 块语句内行首行尾是否要空行
'space-after-keywords': [0, 'always'], // 关键字后面是否要空一格
'space-before-blocks': [0, 'always'], // 不以新行开始的块{前面要不要有空格
'space-before-function-paren': [0, 'always'], // 函数定义时括号前面要不要有空格
'space-in-parens': [0, 'never'], // 小括号里面要不要有空格
'space-infix-ops': 0, // 中缀操作符周围要不要有空格
eqeqeq: 0, // 必须使用全等
'no-var': 0, // 禁用var用let和const代替
'no-inline-comments': 0, // 禁止行内备注
indent: 0,
'vue/script-indent': 0,
'vue/require-prop-type-constructor': 0,
'vue/no-use-v-if-with-v-for': 0,
'no-trailing-spaces': 0, // 一行结束后面不要有空格
'no-multiple-empty-lines': 0, // [1, {"max": 2}],空行最多不能超过2行
'no-extra-boolean-cast': 0, // 禁止不必要的bool转换
'valid-jsdoc': 0,
'one-var': 0, // 连续声明
semi: 0, // 语句强制分号结尾
'semi-spacing': [0, { before: false, after: true }], // 分号前后空格
'no-new': 0, // 禁止在使用new构造一个实例后不赋值
'no-extra-semi': 0, // 禁止多余的冒号
'keyword-spacing': 0,
'arrow-parens': 0, // 箭头函数用小括号括起来 - 关闭
'generator-star-spacing': 0, // 生成器函数*的前后空格
'no-mixed-operators': 0,
'eol-last': 0, // 文件以单一的换行符结束 - 关闭
'object-curly-spacing': 0, // 大括号内是否允许不必要的空格
'no-callback-literal': 0,
'multiline-ternary': 0,
'no-self-assign':'off',
'vue/multi-word-component-names': "off",
"vue/no-v-model-argument": "off",
},
plugins:[
'vue'
],
parserOptions: {
parser: 'babel-eslint',
"requireConfigFile": false,
},
overrides: [
{
files: ['**/__tests__/*.{j,t}s?(x)', '**/tests/unit/**/*.spec.{j,t}s?(x)'],
env: {
jest: true
}
}
]
};

24
.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
.DS_Store
node_modules
/dist
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
dist.rar
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

36
README.en.md Normal file
View File

@@ -0,0 +1,36 @@
# aida_manage
#### Description
{**When you're done, you can delete the content in this README and update the file with details for others getting started with your repository**}
#### Software Architecture
Software architecture description
#### Installation
1. xxxx
2. xxxx
3. xxxx
#### Instructions
1. xxxx
2. xxxx
3. xxxx
#### Contribution
1. Fork the repository
2. Create Feat_xxx branch
3. Commit your code
4. Create Pull Request
#### Gitee Feature
1. You can use Readme\_XXX.md to support different languages, such as Readme\_en.md, Readme\_zh.md
2. Gitee blog [blog.gitee.com](https://blog.gitee.com)
3. Explore open source project [https://gitee.com/explore](https://gitee.com/explore)
4. The most valuable open source project [GVP](https://gitee.com/gvp)
5. The manual of Gitee [https://gitee.com/help](https://gitee.com/help)
6. The most popular members [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/)

5
README.md Normal file
View File

@@ -0,0 +1,5 @@
# aida_manage
#### 介绍
node版本 16.13.2

5
babel.config.js Normal file
View File

@@ -0,0 +1,5 @@
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
]
}

21602
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

68
package.json Normal file
View File

@@ -0,0 +1,68 @@
{
"name": "aida",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"build:test": "vue-cli-service build --mode test",
"lint": "vue-cli-service lint"
},
"dependencies": {
"@ant-design-vue/use": "^0.0.1-alpha.10",
"@vue/compiler-sfc": "^3.2.47",
"ant-design-vue": "^3.2.12",
"axios": "^0.27.2",
"core-js": "^3.8.3",
"js-base64": "^3.7.5",
"md5": "^2.3.0",
"moment": "^2.29.4",
"vue": "^3.2.13",
"vue-class-component": "^8.0.0-0",
"vue-router": "^4.0.3",
"vuedraggable": "^4.1.0",
"vuex": "^4.0.0",
"xlsx": "^0.18.5"
},
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^5.4.0",
"@typescript-eslint/parser": "^5.4.0",
"@vue/cli-plugin-babel": "~5.0.0",
"@vue/cli-plugin-eslint": "~5.0.0",
"@vue/cli-plugin-router": "~5.0.0",
"@vue/cli-plugin-typescript": "~5.0.0",
"@vue/cli-plugin-vuex": "~5.0.0",
"@vue/cli-service": "~5.0.0",
"@vue/eslint-config-typescript": "^9.1.0",
"babel-eslint": "^10.1.0",
"eslint": "^7.32.0",
"eslint-plugin-vue": "^8.0.3",
"less": "^4.1.3",
"less-loader": "^11.0.0",
"style-resources-loader": "^1.5.0",
"typescript": "~4.5.5",
"vue-cli-plugin-style-resources-loader": "^0.1.5",
"vue-lazyload": "^3.0.0-rc.2"
},
"eslintConfig": {
"root": true,
"env": {
"node": true
},
"extends": [
"plugin:vue/vue3-essential",
"eslint:recommended",
"@vue/typescript/recommended"
],
"parserOptions": {
"ecmaVersion": 2020
},
"rules": {}
},
"browserslist": [
"> 1%",
"last 2 versions",
"not dead",
"not ie 11"
]
}

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

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">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title>Mixi</title>
<link href="https://fonts.font.im/css?family=Roboto:400,500,700,700i" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700;900&display=swap" rel="stylesheet">
</head>
<body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/color-thief/2.3.0/color-thief.umd.js"></script>
<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>

11
src/App.vue Normal file
View File

@@ -0,0 +1,11 @@
<template>
<router-view/>
</template>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
</style>

View File

@@ -0,0 +1,115 @@
@font-face {
font-family: "iconfont"; /* Project id 3922178 */
src: url('iconfont.woff2?t=1678713716640') format('woff2'),
url('iconfont.woff?t=1678713716640') format('woff'),
url('iconfont.ttf?t=1678713716640') format('truetype');
}
.iconfont {
font-family: "iconfont" !important;
font-size: 16px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.icon-tuichu1:before {
content: "\e612";
}
.icon-guanbi:before {
content: "\eca0";
}
.icon-shanchu:before {
content: "\e663";
}
.icon-tianjiatupian_huaban:before {
content: "\e62c";
}
.icon-kaiguanswitch-jibenzhuangtai-dianliangzhuangtai:before {
content: "\e641";
}
.icon-gengduo:before {
content: "\e609";
}
.icon-31tishi:before {
content: "\e601";
}
.icon-biaoqian:before {
content: "\e63d";
}
.icon-a-gengduocaidangongneng:before {
content: "\e689";
}
.icon-icon-test:before {
content: "\e668";
}
.icon-xinzengyonghu:before {
content: "\e634";
}
.icon-xialajiantouxiao:before {
content: "\e87e";
}
.icon-8:before {
content: "\e624";
}
.icon-lujing:before {
content: "\e687";
}
.icon-xinzengshangpin:before {
content: "\e72c";
}
.icon-shangpinliebiao:before {
content: "\e61e";
}
.icon-a-zhihuimendianduomendian:before {
content: "\e67d";
}
.icon-shangpinguanli:before {
content: "\e65d";
}
.icon-xiaoshoubaobiao:before {
content: "\e61c";
}
.icon-crmkehuguanli:before {
content: "\e648";
}
.icon-mendianguanli:before {
content: "\e60f";
}
.icon-gongzuotai:before {
content: "\e64a";
}
.icon-xitongguanli:before {
content: "\e614";
}
.icon-weibiaoti:before {
content: "\e616";
}
.icon-yonghuming:before {
content: "\e6aa";
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,184 @@
{
"id": "3922178",
"name": "mixi",
"font_family": "iconfont",
"css_prefix_text": "icon-",
"description": "",
"glyphs": [
{
"icon_id": "8229473",
"name": "退出",
"font_class": "tuichu1",
"unicode": "e612",
"unicode_decimal": 58898
},
{
"icon_id": "6616997",
"name": "关闭",
"font_class": "guanbi",
"unicode": "eca0",
"unicode_decimal": 60576
},
{
"icon_id": "7450626",
"name": "删除",
"font_class": "shanchu",
"unicode": "e663",
"unicode_decimal": 58979
},
{
"icon_id": "12753044",
"name": "添加图片",
"font_class": "tianjiatupian_huaban",
"unicode": "e62c",
"unicode_decimal": 58924
},
{
"icon_id": "8897231",
"name": "开关 switch-基本状态-关闭状态",
"font_class": "kaiguanswitch-jibenzhuangtai-dianliangzhuangtai",
"unicode": "e641",
"unicode_decimal": 58945
},
{
"icon_id": "22334201",
"name": "更多",
"font_class": "gengduo",
"unicode": "e609",
"unicode_decimal": 58889
},
{
"icon_id": "201572",
"name": "3.1-提示",
"font_class": "31tishi",
"unicode": "e601",
"unicode_decimal": 58881
},
{
"icon_id": "1305451",
"name": "标签",
"font_class": "biaoqian",
"unicode": "e63d",
"unicode_decimal": 58941
},
{
"icon_id": "27226073",
"name": "更多,菜单,功能",
"font_class": "a-gengduocaidangongneng",
"unicode": "e689",
"unicode_decimal": 59017
},
{
"icon_id": "1904339",
"name": "头像 男孩",
"font_class": "icon-test",
"unicode": "e668",
"unicode_decimal": 58984
},
{
"icon_id": "9012695",
"name": "新增用户",
"font_class": "xinzengyonghu",
"unicode": "e634",
"unicode_decimal": 58932
},
{
"icon_id": "2076220",
"name": " 下拉箭头小",
"font_class": "xialajiantouxiao",
"unicode": "e87e",
"unicode_decimal": 59518
},
{
"icon_id": "11125634",
"name": "门店",
"font_class": "8",
"unicode": "e624",
"unicode_decimal": 58916
},
{
"icon_id": "12010939",
"name": "商品列表",
"font_class": "lujing",
"unicode": "e687",
"unicode_decimal": 59015
},
{
"icon_id": "17458088",
"name": "新增商品",
"font_class": "xinzengshangpin",
"unicode": "e72c",
"unicode_decimal": 59180
},
{
"icon_id": "26247290",
"name": "商品列表",
"font_class": "shangpinliebiao",
"unicode": "e61e",
"unicode_decimal": 58910
},
{
"icon_id": "26578334",
"name": "智慧门店(多门店)",
"font_class": "a-zhihuimendianduomendian",
"unicode": "e67d",
"unicode_decimal": 59005
},
{
"icon_id": "1277993",
"name": "商品管理",
"font_class": "shangpinguanli",
"unicode": "e65d",
"unicode_decimal": 58973
},
{
"icon_id": "1964730",
"name": "销售报表",
"font_class": "xiaoshoubaobiao",
"unicode": "e61c",
"unicode_decimal": 58908
},
{
"icon_id": "9748102",
"name": "crm客户管理",
"font_class": "crmkehuguanli",
"unicode": "e648",
"unicode_decimal": 58952
},
{
"icon_id": "12250139",
"name": "门店管理",
"font_class": "mendianguanli",
"unicode": "e60f",
"unicode_decimal": 58895
},
{
"icon_id": "20022023",
"name": "工作台",
"font_class": "gongzuotai",
"unicode": "e64a",
"unicode_decimal": 58954
},
{
"icon_id": "27253679",
"name": "系统管理",
"font_class": "xitongguanli",
"unicode": "e614",
"unicode_decimal": 58900
},
{
"icon_id": "6400627",
"name": "密码1",
"font_class": "weibiaoti",
"unicode": "e616",
"unicode_decimal": 58902
},
{
"icon_id": "20853365",
"name": "用户名",
"font_class": "yonghuming",
"unicode": "e6aa",
"unicode_decimal": 59050
}
]
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,17 @@
@font-face {
font-family: "iconfont"; /* Project id */
src: url('iconfont.ttf?t=1709533486156') format('truetype');
}
.iconfont {
font-family: "iconfont" !important;
font-size: 16px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.icon-xiazaiwenjian:before {
content: "\e600";
}

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

197
src/assets/style/style.less Normal file
View File

@@ -0,0 +1,197 @@
html,body,#app{
margin: 0;
padding: 0;
height: 100%;
font-family: 'Roboto', sans-serif;
}
.list_page{
padding-left: 28px;
width: 100%;
height: 100%;
.list_page_content{
background: #fff;
width: 100%;
height: 100%;
padding: 20px 0 50px;
box-sizing: border-box;
.list_top_content{
padding: 0 35px 0 28px;
margin-bottom: 20px;
display: flex;
justify-content: space-between;
.list_top_search_content{
width: 100%;
}
.list_top_button_content{
width: 200px;
flex-shrink: 0;
}
.list_top_button_content{
width: 200px;
}
}
.list_table_content{
width: 100%;
}
}
}
.edit_modal_component{
.modal_button_list{
margin-top: 20px;
display: flex;
align-items: center;
}
}
.primary_button{
background: #343579;
border-color:#343579;
}
.ant-btn-primary:hover, .ant-btn-primary:focus{
border-color: #343579;
}
.default_button{
background: #E4E5EB;
}
.btn-margin-r-20{
margin-right: 20px;
}
.btn-margin-t-35{
margin-top: 35px;
}
.confirm_style{
.ant-btn-primary{
background: #343579;
}
}
.form_item_block{
position: relative;
.form_title_button_list{
position: absolute;
top: -8px;
right: 0;
}
.form_item_module_title{
font-size: 18px;
line-height: 18px;
font-weight: normal;
color: #030303;
margin-bottom: 20px;
}
}
.sec_form_block{
margin-bottom: 20px;
}
.form_item_title{
color: #030303;
font-size: 18px;
line-height: 1.5715;
overflow: hidden;
&.required::before{
display: inline-block;
margin-right: 4px;
color: #ff4d4f;
font-size: 14px;
font-family: SimSun, sans-serif;
line-height: 1;
content: '*';
}
.title_right_button{
float: right;
}
}
.ant-modal-title{
font-size: 18px;
font-weight: bold;
color: #030303;
}
.ant-form-item-label > label{
font-size: 18px;
color: #030303;
}
.scroll_style{
&::-webkit-scrollbar-button:single-button {
// background-color: #ffffff;
display: block;
border-style: solid;
height: 13px;
width: 14px;
}
&::-webkit-scrollbar-button:single-button:vertical:decrement {
border-width: 0 8px 8px 8px;
border-color: transparent transparent #555555 transparent;
}
&::-webkit-scrollbar-button:single-button:vertical:decrement:hover {
border-color: transparent transparent #777777 transparent;
}
&::-webkit-scrollbar-button:single-button:vertical:increment {
border-width: 0 8px 8px 8px;
border-color: #555555 transparent transparent transparent;
}
&::-webkit-scrollbar-button:vertical:single-button:increment:hover {
border-color: #777777 transparent transparent transparent;
}
&::-webkit-scrollbar {
/*滚动条整体样式*/
width: 14px; /*高宽分别对应横竖滚动条的尺寸*/
}
&::-webkit-scrollbar-thumb {
/*滚动条里面小方块*/
background: #c2c2c2;
opacity: 0.8;
border-radius: 7px;
}
&::-webkit-scrollbar-track {
// background: #ffffff;
}
}
.ant-upload-list-picture-card-container{
display: none !important;
}
//蒙层样式
.mark_loading{
position: fixed;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.2);
left: 0;
top: 0;
z-index: 99999;
display: flex;
align-items: center;
justify-content: center;
}

View File

@@ -0,0 +1,74 @@
<template>
<div class="filter_component">
<div class="filter_component_content">
<span class="filter_title" :style="titleStyle">{{title}}</span>
<div class="slot_content" :style="slotStyle">
<slot class="slot_style"></slot>
</div>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent,ref } from "vue";
export default defineComponent({
name:'filterComponent',
props:{
title:{
type:String
},
titleStyle:{
type:Object
},
slotStyle:{
type:Object
}
},
setup(){
return {
}
},
});
</script>
<style lang="less">
.filter_component{
display: inline-block;
margin-right: 40px;
margin-bottom: 30px;
.filter_component_content{
display: flex;
align-items: center;
.filter_title{
font-size: 16px;
font-family: Roboto;
font-weight: 400;
color: #030303;
margin-right: 15px;
flex-shrink: 0;
display: block;
min-width: 150px;
text-align: right;
}
.slot_content{
width: 280px;
.ant-picker-large{
width: 100%;
}
}
}
}
</style>

View File

@@ -0,0 +1,385 @@
<template>
<div class="add_product_modal">
<a-modal class="add_product_modal_component"
v-model:visible="addProductModal"
:footer="null"
title="Add Product"
width="680px"
:closable="false"
:destroyOnClose="true"
:maskClosable="false"
:centered="true"
:keyboard="false"
>
<a-form ref="formRef" :model="formState" :rules="rules" :layout="'vertical'" >
<a-form-item label="Add picture" name="pictureList">
<div class="picture_list">
<div class="picture_list_block" v-show="showPicture.length">
<div class="pricture_item_block" v-for="pic in showPicture" :key="pic">
<img class="pricture_item" :src="pic?.resData.pictureUrl">
</div>
</div>
<div class="pricture_item_block add_pic_button" @click="openUploadModal">
<div class="add_pic_button_content">
<span class="icon iconfont add_pic_button_icon icon-tianjiatupian_huaban"></span>
<div>{{showPicture.length ? '继续上传' :'上传'}}</div>
</div>
</div>
</div>
</a-form-item>
<a-form-item label="Label attributes" name="productLabel">
<a-select v-model:value="formState.productLabel" mode="multiple" size="large" optionFilterProp="label" :options="productLabelList" showArrow></a-select>
</a-form-item>
<!-- <a-form-item label="Price" name="price">
<a-input-number
style="width:100%"
v-model:value="formState.price"
:formatter="value => `$ ${value}`.replace(/\B(?=(\d{3})+(?!\d))/g, ',')"
:parser="value => value.replace(/\$\s?|(,*)/g, '')"
/>
</a-form-item>
<a-form-item label="Store Inventory" name="storeInventory">
<div class="store_inventory_block">
<div class="store_inventory_content">{{storeNameList}}</div>
<a-button class="primary_button" type="primary" @click="selectStore">Choose store</a-button>
</div>
</a-form-item> -->
</a-form>
<div class="modal_button_list">
<a-button class="default_button btn-margin-r-20" size="large" @click="closeProduct">Cancel</a-button>
<a-button class="primary_button" type="primary" size="large" :loading="submitLoading" @click="submitAddProduct">Submit</a-button>
</div>
<!-- design collection的进度蒙层 start-->
<div class="progress_mark" v-show="showDesignMark">
<div class="mark_content">
<a-progress type="circle" :percent="designProgress" :width="200"/>
<div class="mark_content_spin">
<a-spin :indicator="indicator"/>
</div>
<div class="upload_tip" v-show="uploadType == 'Attribute'">Recognizing garment, please wait.</div>
<div class="upload_tip" v-show="uploadType == 'Collocation'">Generating outfit, please wait.</div>
</div>
</div>
<!-- design collection的进度蒙层 end-->
</a-modal>
<ProductPicUpload ref="productPicUploadModal" @uploadPicChange="uploadPicChange"></ProductPicUpload>
<AddStoreModal ref="addStoreModalComponent" @storeChange="storeChange"></AddStoreModal>
</div>
</template>
<script lang="ts">
import { defineComponent,h,ref,reactive,toRefs,onMounted, computed,createVNode } from "vue";
import ProductPicUpload from '@/component/productComponent/productPicUpload.vue'
import AddStoreModal from '@/component/productComponent/addStoreModa.vue'
import { Https } from "@/tool/https";
import { message,Modal } from "ant-design-vue";
import {unique} from '@/tool/util'
import { WarningOutlined,LoadingOutlined } from '@ant-design/icons-vue';
export default defineComponent({
name:'AddProduct',
components:{ProductPicUpload,AddStoreModal},
emits: ['confirmSubmitAdd'],
props:{
productLabelList:{
type:Array,
}
},
setup(props,{ emit }){
let formRef = ref();
let indicator :any = h(LoadingOutlined, {
style: {
fontSize: '24px',
},
spin: true,
})
let addProductModal = ref(false)
let showDesignMark = ref(false)
let designProgress:any = ref(0)
let uploadType:any = ref('Attribute')
let addStoreModal = ref(false)
let productPicUploadModal:any = ref(null)
let formState:any = ref({
pictureList: [],
productLabel:[],
price:'',
storeInventory:'',
});
let submitLoading = ref(false)
let addStoreModalComponent:any = ref(null)
let storeNameList:any = ref('')
let showPicture = computed(()=> formState.value.pictureList.length > 4 ? formState.value.pictureList.slice(0,4) : formState.value.pictureList)
let rules = {
pictureList: [
{ required: true, message: 'Please upload product image', trigger: 'blur' },
],
productLabel: [
{ required: true, message: 'Please select product type', trigger: 'blur' },
],
price: [
{ required: true, message: 'Please input price', trigger: 'blur' },
],
storeInventory:[
{ required: true, message: 'Please select store',trigger: 'blur' },
]
}
let showModal = (data:any) =>{
addProductModal.value = true
}
let closeProduct = () =>{
Modal.confirm({
title: "Are you sure you want to close the popup? Closing the popup will clear the set data.",
icon: createVNode(WarningOutlined),
class:'confirm_style',
okText: 'Ok',
cancelText: 'Cancel',
// centered:true,
onOk() {
formState.value = {
pictureList: '',
productLabel:[],
storeInventory:'',
}
clearChildData()
storeNameList.value = ''
addProductModal.value = false
}
});
}
let openUploadModal = () =>{
productPicUploadModal.value.openPictureModal()
}
let selectStore = () =>{
addStoreModalComponent.value.openAddStoreModal({type:'add'})
}
let uploadPicChange = (event:any) =>{
formState.value.pictureList = event.fileList
formRef.value.validateFields(['pictureList'])
}
let storeChange = (event:any) =>{
formState.value.storeInventory = event
storeNameList.value = event.map((v:any) => v.storeName).join(',')
formRef.value.validateFields(['storeInventory'])
}
let submitAddProduct = () =>{
formRef.value.validate().then(()=>{
submit()
})
let submit = () =>{
let pictureList = unique(formState.value.pictureList, 'md5')
let productIds = pictureList.map((v:any)=>{
return v.resData.productId
})
let data = {
labelIds:formState.value.productLabel,
productIds:productIds,
// price:formState.value.price,
// storeInfos:formState.value.storeInventory,
timeZone:Intl.DateTimeFormat().resolvedOptions().timeZone,
}
submitLoading.value = true
showDesignMark.value = true
Https.axiosPost(Https.httpUrls.batchUploadProductRelation, data).then(
(rv: any) => {
if (rv) {
submitLoading.value = false
showDesignMark.value = false
emit('confirmSubmitAdd')
formState.value = {
pictureList: '',
productLabel:[],
storeInventory:'',
}
clearChildData()
addProductModal.value = false
storeNameList.value = ''
designProgress.value = 0
uploadType.value = 'Attribute'
}
}
).catch((rv:any)=>{
submitLoading.value = false
showDesignMark.value = false
designProgress.value = 0
uploadType.value = 'Attribute'
});
submitProcess()
}
}
let submitProcess = () =>{
Https.axiosPost(Https.httpUrls.countProductUpdateProcess, {}).then(
(rv: any) => {
if(rv.status != 1 && showDesignMark.value){
let designProgressNew:any = rv.process * 100
designProgress.value = parseInt(designProgressNew)
uploadType.value = rv.uploadType
setTimeout(()=>{
submitProcess()
},2000)
}
}
);
}
//清除子组件的数据
let clearChildData = () =>{
productPicUploadModal.value.clearPicData()
addStoreModalComponent.value.clearStoreData()
}
onMounted(() => {
})
return {
formRef,
indicator,
addProductModal,
showDesignMark,
designProgress,
uploadType,
addStoreModal,
productPicUploadModal,
submitLoading,
showModal,
storeNameList,
formState,
showPicture,
rules,
closeProduct,
openUploadModal,
selectStore,
addStoreModalComponent,
uploadPicChange,
storeChange,
submitAddProduct,
}
},
});
</script>
<style lang="less" scoped>
.add_product_modal_component{
.picture_list{
display: flex;
.picture_list_block{
display: flex;
margin-right: 20px;
}
.pricture_item_block{
width: 130px;
height: 130px;
display: flex;
align-items: center;
justify-content: center;
margin-left: -13px;
&.add_pic_button{
background: #F7F8FC;
border: 1px solid #DFDFDF;
cursor: pointer;
margin: 0;
}
.add_pic_button_content{
text-align: center;
color: #C7CDD5;
font-size: 16px;
.add_pic_button_icon{
font-size: 36px;
margin: 0 auto 15px;
}
}
&:first-child{
margin-left: 0;
}
.pricture_item{
max-height: 100%;
max-width: 100%;
}
}
}
.progress_mark{
width: 100%;
height: 100%;
background: #fff;
position: absolute;
left: 0;
top: 0;
display: flex;
align-items: center;
justify-content: center;
.mark_content{
text-align: center;
.mark_content_spin{
margin-top: 20px;
}
.upload_tip{
font-size: 14px;
text-align: center;
margin-top: 10px;
}
}
}
.store_inventory_block{
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
height: 46px;
border: 1px solid #DFDFDF;
padding: 0 12px;
.store_inventory_content{
width: 500px;
flex-shrink: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
}
.add_store_modal_component{
.modal_button_list{
margin-top: 20px;
}
.store_input{
text-align: center;
}
}
</style>

View File

@@ -0,0 +1,290 @@
<template>
<div class="add_product_modal">
<a-modal class="add_store_modal_component"
v-model:visible="addStoreModal"
:footer="null"
title="Choose store"
width="1080px"
:destroyOnClose="true"
:closable="false"
:maskClosable="false"
:centered="true"
>
<a-table :columns="columns" :data-source="storeList" bordered
:pagination="false" :scroll="{y: 400 }">
<template v-slot:bodyCell="{column,record}">
<template v-if="column.dataIndex === 'storeName'">
<a-checkbox v-model:checked="record.storeCheck">{{record.storeName}}</a-checkbox>
</template>
<template v-for="item in storeNameData">
<template v-if="column.dataIndex === item">
<a-input class="store_input" v-model:value="record[item]" type="number" />
</template>
</template>
</template>
<!-- <template v-slot:bodyCell="{column,record}" >
<template v-if="column.dataIndex === 'storeName'">
<a-checkbox v-model:checked="record.storeCheck">{{record.storeName}}</a-checkbox>
</template>
<template v-if="column.dataIndex === 'XS'">
<a-input class="store_input" v-model:value="record.XS" type="number" :min="0"/>
</template>
<template v-if="column.dataIndex === 'S'">
<a-input class="store_input" v-model:value="record.S" type="number" :min="0"/>
</template>
<template v-if="column.dataIndex === 'M'">
<a-input class="store_input" v-model:value="record.M" type="number" :min="0"/>
</template>
<template v-if="column.dataIndex === 'L'">
<a-input class="store_input" v-model:value="record.L" type="number" :min="0"/>
</template>
<template v-if="column.dataIndex === 'XL'">
<a-input class="store_input" v-model:value="record.XL" type="number" :min="0"/>
</template>
<template v-if="column.dataIndex === 'XXL'">
<a-input class="store_input" v-model:value="record.XXL" type="number" :min="0"/>
</template>
</template> -->
</a-table>
<div class="modal_button_list">
<a-button class="default_button btn-margin-r-20" size="large" @click="closeProduct">Cancel</a-button>
<a-button class="primary_button" type="primary" size="large" @click="submitStore">Submit</a-button>
</div>
</a-modal>
</div>
</template>
<script lang="ts">
import { defineComponent,ref,reactive,toRefs,onMounted, computed,createVNode } from "vue";
import { Https } from "@/tool/https";
import { message,Modal } from "ant-design-vue";
import { WarningOutlined } from '@ant-design/icons-vue';
export default defineComponent({
name:'AddStoreModal',
emits: ['storeChange'],
setup(props,{ emit }){
let addStoreModal = ref(false)
let addStoreType = ref('add')
let columns = reactive([
{ title: 'Store', align:'center', ellipsis: true, dataIndex: 'storeName', key: 'storeName',width:"200px" },
{
title: 'Inventory',
align:'center',
ellipsis: true,
dataIndex: 'inventory',
key: 'inventory',
children: [
// {title: 'XS',dataIndex: 'XS',key: 'XS', align:'center', },
// {title: 'S',dataIndex: 'S',key: 'S', align:'center',},
// {title: 'M',dataIndex: 'M',key: 'M', align:'center',},
// {title: 'L',dataIndex: 'L',key: 'L', align:'center',},
// {title: 'XL',dataIndex: 'XL',key: 'XL', align:'center',},
// {title: 'XXL',dataIndex: 'XXL',key: 'XXL', align:'center',},
],
},
])
let storeList:any = ref([])
let backupStoreList:any = ref([])//提交时备份的数据
let storeNameData:any = ref([])
let openAddStoreModal = async (data:any) =>{
if(data.type === 'add'){
if(backupStoreList.value.length){
storeList.value = JSON.parse(JSON.stringify(backupStoreList.value))
}
}else{
let storeIds:any = []
for(let item of data.storeData){
storeIds.push(item.storeId)
}
let children:any = []
data.storeNameData.forEach((item:any) => {
children.push({
title:item,
dataIndex:item,
key:item,
align:'center'
})
});
columns[1].children = children
storeNameData.value = data.storeNameData
await storeQueryAll()
storeList.value = backupStoreList.value.filter((v:any) => storeIds.indexOf(v.id) == -1)
console.log(storeList.value);
}
addStoreModal.value = true
addStoreType.value = data.type
}
let submitStore= () => {
if(addStoreType.value === 'add'){
addStore()
}else{
editStore()
}
}
let addStore = () =>{
let field:any = JSON.parse(JSON.stringify(storeNameData.value))
let storeInfos = []
for(let v of storeList.value){
if(v.storeCheck){
let data:any = {
stock:[],
storeId:v.id,
storeName:v.storeName,
}
for(let key in v){
if(field.indexOf(key) > -1){
let sizeData:any = {
num:v[key],
size:key
}
data.stock.push(sizeData)
}
}
storeInfos.push(data)
}
}
backupStoreList.value = JSON.parse(JSON.stringify(storeList.value))
emit('storeChange',storeInfos)
addStoreModal.value = false
}
let editStore = () =>{
let storeInfos = storeList.value.filter((v:any) => v.storeCheck)
emit('storeChange',storeInfos)
addStoreModal.value = false
}
let closeProduct = () =>{
Modal.confirm({
title: "To confirm the cancellation of setting store information? Cancellation will clear the setting data for this time.",
icon: createVNode(WarningOutlined),
class:'confirm_style',
okText: 'Ok',
cancelText: 'Cancel',
// centered:true,
onOk() {
storeList.value = JSON.parse(JSON.stringify(backupStoreList.value))
addStoreModal.value = false
}
});
}
let storeQueryAll = async () =>{
let storeData:any = {
storeCheck:false,
}
await new Promise((resolve,reject)=>{
storeNameData.value.forEach((item:any) => {
storeData[item] = 0
});
console.log(storeData);
Https.axiosPost(Https.httpUrls.queryProductStore,{}).then(
(rv: any) => {
if (rv) {
storeList.value = rv.map((v:any)=>{
let data = {
...v,
storeName:v.name,
storeId:v.id,
...storeData,
}
return data
})
backupStoreList.value = JSON.parse(JSON.stringify(storeList.value))
resolve('')
}
}
);
})
}
let clearStoreData = () =>{
storeQueryAll()
}
onMounted(() => {
// storeQueryAll()
})
return {
addStoreModal,
storeNameData,
columns,
storeList,
openAddStoreModal,
closeProduct,
submitStore,
clearStoreData,
}
},
});
</script>
<style lang="less" scoped>
.add_product_modal_component{
.picture_list{
display: flex;
.pricture_item_block{
width: 130px;
height: 130px;
display: flex;
align-items: center;
justify-content: center;
margin-left: -30px;
&.add_pic_button{
background: #F7F8FC;
border: 1px solid #DFDFDF;
cursor: pointer;
}
.add_pic_button_content{
text-align: center;
color: #C7CDD5;
font-size: 16px;
.add_pic_button_icon{
font-size: 36px;
margin: 0 auto 15px;
}
}
&:first-child{
margin-left: 0;
}
.pricture_item{
max-height: 100%;
max-width: 100%;
width: 100%;
}
}
}
}
.add_store_modal_component{
.modal_button_list{
margin-top: 20px;
}
.store_input{
text-align: center;
}
}
</style>

View File

@@ -0,0 +1,769 @@
<template>
<div class="add_product_modal">
<a-modal class="add_product_modal_component"
wrapClassName="edit_modal"
:destroyOnClose="true"
v-model:visible="editProductModal"
:footer="null"
title="Eidt Product"
width="900px"
:maskClosable="false"
:centered="true"
@cancel="closeProduct"
>
<a-form ref="formRef" :model="formState" :rules="rules" :layout="'vertical'" >
<a-form-item label="Product image" name="pictureUrl">
<div class="product_master_diagram">
<a-upload
:action="uploadUrl + '/api/product/uploadFile'"
class="edit_product_pic"
list-type="picture-card"
:data="{
...upload,
id:productDetail.id
}"
:headers="{Authorization:token}"
v-model:file-list="fileList"
:maxCount="1"
accept=".jpg,.png,.jpeg,.bmp"
@change="(file)=>fileUploadChange(file)"
>
<img class="product_master_img" :src="formState.pictureUrl">
</a-upload>
</div>
</a-form-item>
<a-form-item label="Product label(s)" name="productLabel">
<a-select v-model:value="formState.productLabel" mode="multiple" size="large" :options="productLabelList" :getPopupContainer="(triggerNode) => getPopupContainer(triggerNode)" optionFilterProp="label"></a-select>
</a-form-item>
<a-form-item label="Price" name="price">
<a-input-number
style="width:100%"
v-model:value="formState.price"
:formatter="value => `$ ${value}`.replace(/\B(?=(\d{3})+(?!\d))/g, ',')"
:parser="value => value.replace(/\$\s?|(,*)/g, '')"
/>
</a-form-item>
<div class="form_item_block">
<!-- <a-form-item label="库存信息" name="inventoryData"></a-form-item> -->
<div class="form_item_title required">
Inventory
<!-- <div class="title_right_button">
<a-button class="primary_button" type="primary" @click="addStore">+ Add stores</a-button>
</div> -->
</div>
<div class="form_edit_table">
<a-table :columns="columns" :data-source="formState.storeList" bordered
:pagination="false" :scroll="{y: 400 }">
<template v-slot:bodyCell="{column,record}">
<template v-for="item in formState.storeNameList">
<template v-if="column.dataIndex === item">
<a-input class="store_input" v-model:value="record[item]" type="number" />
</template>
</template>
</template>
<!-- <template v-slot:bodyCell="{column,record}" >
<template v-if="column.dataIndex === 'XS'">
<a-input class="store_input" v-model:value="record.XS" type="number" />
</template>
<template v-if="column.dataIndex === 'S'">
<a-input class="store_input" v-model:value="record.S" type="number" />
</template>
<template v-if="column.dataIndex === 'M'">
<a-input class="store_input" v-model:value="record.M" type="number" />
</template>
<template v-if="column.dataIndex === 'L'">
<a-input class="store_input" v-model:value="record.L" type="number" />
</template>
<template v-if="column.dataIndex === 'XL'">
<a-input class="store_input" v-model:value="record.XL" type="number" />
</template>
<template v-if="column.dataIndex === 'XXL'">
<a-input class="store_input" v-model:value="record.XXL" type="number" />
</template>
</template> -->
</a-table>
</div>
</div>
<div class="form_item_block">
<div class="form_item_module_title">Label attributes</div>
<a-form-item label="Select category" name="productCategory">
<a-select v-model:value="formState.productCategory" size="large" :options="productCategoryList" placeholder="Please select" @change="productCategoryChange" :getPopupContainer="(triggerNode) => getPopupContainer(triggerNode)" optionFilterProp="label"></a-select>
</a-form-item>
<div class="sec_form_block">
<div class="form_item_title margin_bottom_10">Attribute(s)</div>
<div class="attribute_block" v-if="productAttributeList.length">
<div class="attribute_block_item" v-for="attr in productAttributeList" :key="attr.id">
<div class="attribute_block_item_content">
<div class="attribute_item_title">{{attr.labelType}}</div>
<a-select class="attribute_item_select" allowClear v-model:value="attr.value" size="large" :options="attr.attributeValueList" :getPopupContainer="(triggerNode) => getPopupContainer(triggerNode)"></a-select>
</div>
</div>
</div>
<div v-else class="null_data_block">
<img class="null_data_img" src="@/assets/images/null_img.png">
</div>
</div>
</div>
<div class="form_item_block">
<!-- <a-form-item label="库存信息" name="inventoryData"></a-form-item> -->
<div class="form_item_title">
Mix and Match(es)
<div class="title_right_button">
<a-button class="primary_button" type="primary" @click="addLook">+ Add Look</a-button>
</div>
</div>
<div class="product_match_block" v-show="formState.assortmentList.length">
<div class="product_match_item" v-for="(match,index) in formState.assortmentList" :key="match" v-show="index<5 || assortmentShowMore">
<div class="product_match_item_header">
<div>Look{{index+1}}</div>
<div class="item_header_button_list">
<a-button class="primary_button btn-margin-r-20" type="primary" @click="deleteSingleAllMatch(index)">Delete</a-button>
<a-button class="primary_button" type="primary" @click="addProductMatch(match,index)" v-show="match.length<6">Add Item</a-button>
</div>
</div>
<div class="product_match_item_content">
<div class="match_item_img_block" v-for="(img,imgIndex) in match" :key="img.id">
<img :src="img.generatePictureUrl">
<div class="delete_img_block" @click="deleteSignleMatchImg(index,imgIndex)">
<span class="icon iconfont delete_icon icon-guanbi"></span>
</div>
</div>
</div>
</div>
<a-button class="primary_button show_more_button" type="primary" @click="assortmentListShowMore()" v-show="formState.assortmentList.length>5">{{assortmentShowMore?'Hide':'See More'}}</a-button>
</div>
</div>
</a-form>
<div class="modal_button_list">
<a-button class="default_button btn-margin-r-20" size="large" @click="closeProduct">Cancel</a-button>
<a-button class="primary_button" type="primary" size="large" @click="submitProduct" :loading="submitLoading">Submit</a-button>
</div>
</a-modal>
<ProductMatchModal ref="ProductMatchModalComponent" :productCategoryList="productCategoryList" @selectMatchFile="selectMatchFile"></ProductMatchModal>
<AddStoreModal ref="addStoreModalComponent" @storeChange="storeChange"></AddStoreModal>
</div>
</template>
<script lang="ts">
import { defineComponent,ref,reactive,toRefs,onMounted, computed, nextTick } from "vue";
import ProductMatchModal from '@/component/productComponent/productMatchModal.vue'
import AddStoreModal from '@/component/productComponent/addStoreModa.vue'
import {getCookie} from '@/tool/cookie'
import {getUploadUrl} from '@/tool/util'
import { Https } from "@/tool/https";
import { message,Modal } from "ant-design-vue";
export default defineComponent({
name:'EditProduct',
components:{ProductMatchModal,AddStoreModal},
props:{
productLabelList:{
type:Array,
}
},
emits:['refreshList'],
setup(props,{ emit }){
let formRef = ref();
let submitLoading:any = ref(false)
let addStoreModalComponent:any = ref(null)
let uploadUrl:any = ref('')
let fileList:any = ref([])
let token:any = ref('')
let upload:any = reactive({
timeZone:Intl.DateTimeFormat().resolvedOptions().timeZone,
})
let editProductModal = ref(false)
let productPicSeletcModal:any = ref()
let ProductMatchModalComponent:any =ref()
let productDetail:any =ref({})
let formState:any = ref({
pictureName:'',
pictureUrl:'',
productLabel:[],
inventoryData:[],
storeList:[
],
storeNameList:[],
productCategory:[],
productAttribute:[],
assortmentList:[]
});
let productCategoryList:any = ref([])
let productAttributeList = ref([])
let columns = reactive([
{ title: 'Store', align:'center', ellipsis: true, dataIndex: 'storeName', key: 'storeName',width:"200px" },
{
title: 'Inventory',
align:'center',
ellipsis: true,
dataIndex: 'inventory',
key: 'inventory',
children: [
// {title: 'XS',dataIndex: 'XS',key: 'XS', align:'center', },
// {title: 'S',dataIndex: 'S',key: 'S', align:'center',},
// {title: 'M',dataIndex: 'M',key: 'M', align:'center',},
// {title: 'L',dataIndex: 'L',key: 'L', align:'center',},
// {title: 'XL',dataIndex: 'XL',key: 'XL', align:'center',},
// {title: 'XXL',dataIndex: 'XXL',key: 'XXL', align:'center',},
],
},
])
let addMatchIndex:number = 0
let rules = {
pictureUrl: [
{ required: true, trigger: 'blur' },
],
productLabel: [
{ required: true, message: 'Please select product label', trigger: 'blur' },
],
inventoryData:[
{ required: true, trigger: 'blur' },
],
productCategory:[
{ required: true, message: 'Please select', trigger: 'blur' },
],
productAttribute:[
{ required: true, message: 'Please select', trigger: 'blur' },
]
}
let assortmentShowMore = ref(false)
let getPopupContainer = (triggerNode:any) =>{
return triggerNode.parentNode || document.body
}
let closeProduct = () =>{
formState.value = {
pictureName:'',
pictureUrl:'',
productLabel:[],
inventoryData:[],
storeList:[],
productCategory:[],
productAttribute:[],
assortmentList:[],
storeNameList:[],
}
productAttributeList.value =[]
editProductModal.value = false
addStoreModalComponent.value.clearStoreData()
}
let showModal = (data:any) =>{
let record = JSON.parse(JSON.stringify(data.record))
let store = getStoreInfo(record.storeInfoList)
let labelItemList = JSON.parse(JSON.stringify(data.labelItemList))
editProductModal.value = true
productDetail.value = record
formState.value = {
pictureName:record.pictureName,
pictureUrl:record.pictureUrl,
productLabel:record.productLabelInfo.map((v:any)=>v.id),
price:record.price,
productCategory:record.attributeItemInfo.labelItem,
storeList:store.storeList,
storeNameList:store.storeNameList,
assortmentList:record.assortmentList,
}
productCategoryList.value = getProductCategoryList(labelItemList)
productAttributeList.value = getProductAttributeList()
formState.value.productAttribute = productAttributeList.value
}
let getProductAttributeList = () => {
let attributeList = productCategoryList.value.filter((v:any) => v.labelItem == formState.value.productCategory)
if(attributeList && attributeList.length){
attributeList = attributeList[0]
}else{
return []
}
let attributeValueList = attributeList.attributeTypeList.map((v:any) =>{
v.value = ''
for(let item of productDetail.value.attributeItemInfo.attributeTypeList){
if(v.labelType === item.labelType){
v.value = item.attributeValueList.join(',')
}
}
return v
})
return attributeValueList
}
let getProductCategoryList =(labelItemList:any)=>{
let newLabelItemList = []
let labelItemListNew = JSON.parse(JSON.stringify(labelItemList))
for(let item of labelItemListNew){
item.attributeTypeList = item.attributeTypeList.map((v:any) =>{
let data = {
...v,
value:v.value || [],
attributeValueList:v.attributeValueList.map((v:any)=>{
let valueData = {
value:v,
label:v,
}
return valueData
})
}
return data
})
newLabelItemList.push(item)
}
return newLabelItemList
}
let getStoreInfo = (storeInfo:any) =>{
let storeList = []
let storeNameList:any = []
for(let item of storeInfo){
let sizeData:any = {}
for(let size of item.stock){
sizeData[size.size] = size.num
}
let data = {
storeName:item.storeName,
storeId:item.storeId,
...sizeData,
}
storeList.push(data)
}
storeInfo.forEach((item:any) => {
item.stock.forEach((stockItem:any) => {
storeNameList.push(stockItem.size);
});
});
// 排序并去重
storeNameList = storeNameList.sort().filter((value:any, index:any, array:any) => {
return array.indexOf(value) === index;
});
let children:any = []
storeNameList.forEach((item:any) => {
children.push({
title:item,
dataIndex:item,
key:item,
align:'center'
})
});
columns[1].children = children
return {storeList,storeNameList}
}
let openUploadModal = () =>{
productPicSeletcModal.value.openPictureModal()
}
//删除单条的全部搭配
let deleteSingleAllMatch = (index:number) =>{
formState.value.assortmentList.splice(index,1)
}
let deleteSignleMatchImg = (matchindex:any,index:number) =>{
if(formState.value.assortmentList[matchindex].length == 1){
deleteSingleAllMatch(matchindex)
}else{
formState.value.assortmentList[matchindex].splice(index,1)
}
}
let addProductMatch = (match:any,index:number) =>{
addMatchIndex = index
let assortmentIdList = match.map((v:any)=>{
return v.id
})
ProductMatchModalComponent.value.openMatchModal(assortmentIdList,productCategoryList.value)
}
let submitStore= () => {
}
let productCategoryChange = (value:any,selectedOptions:any) => {
let attributeList = selectedOptions ? selectedOptions.attributeTypeList :[]
productAttributeList.value = attributeList
formState.value.productAttribute = attributeList
}
let fileUploadChange = (data:any) =>{
let file = data.file
if(file.status === 'done'){
let res = JSON.parse(file.xhr.response)
if(res.data){
productDetail.value.md5 = res.data.md5
formState.value.pictureUrl = res.data.pictureUrl
}else{
message.error(res.errMsg)
}
}else if(file.status === 'error'){
message.error(file.name + 'upload failed')
}
}
let addStore = () =>{
let data = {
type:'edit',
storeData:formState.value.storeList,
storeNameData:formState.value.storeNameList,
}
addStoreModalComponent.value.openAddStoreModal(data)
}
let storeChange = (event:any) =>{
formState.value.storeList = formState.value.storeList.concat(event)
}
let submitProduct = () =>{
formRef.value.validate().then(()=>{
submit()
})
let submit = () =>{
let data = {
id:productDetail.value.id,
pictureName:formState.value.pictureName,
pictureUrl:formState.value.pictureUrl,
price:formState.value.price,
productLabelInfo:getSubmitProductLable(),
storeInfoList:getStoreList(),
attributeItemInfo:getAttributeValueList(),
assortmentList:getAssortmentList(),
md5:productDetail.value.md5,
timeZone:Intl.DateTimeFormat().resolvedOptions().timeZone,
}
submitLoading.value = true
Https.axiosPost(Https.httpUrls.productEdit, data).then(
(rv: any) => {
if (rv) {
submitLoading.value = false
message.success('Edit success')
emit('refreshList')
closeProduct()
}
}
).catch(rv=>{
submitLoading.value = false
});
}
}
let getAttributeValueList = () =>{
let data:any = {
labelItem:formState.value.productCategory
}
let attributeTypeList = formState.value.productAttribute.map((v:any)=>{
let newData = {
id:v.id,
labelType:v.labelType,
attributeValueList:v.value && v.value.length ? [v.value] : []
}
return newData
})
data.attributeTypeList = attributeTypeList
return data
}
let getStoreList = () =>{
let field:any = formState.value.storeNameList
let storeInfos = []
for(let v of formState.value.storeList){
let data:any = {
stock:[],
storeId:v.storeId,
storeName:v.storeName
}
for(let key in v){
if(field.indexOf(key) > -1){
let sizeData:any = {
num:v[key],
size:key
}
data.stock.push(sizeData)
}
}
storeInfos.push(data)
}
return storeInfos
}
let getSubmitProductLable = () =>{
let labelList:any = props.productLabelList?.filter((v:any)=>formState.value.productLabel.indexOf(v.id) > -1)
labelList = labelList.map((v:any) => {
return {
id:v.id,
name:v.name,
type:v.type,
}
})
labelList = labelList.length ? labelList :productDetail.value.productLabelInfo
return labelList
}
let getAssortmentList = () =>{
let assortmentList = formState.value.assortmentList.map((v:any,index:any)=>{
let data = v
for(let item of data){
item.coverSort = index + 1
}
return data
})
return assortmentList
}
//选择搭配的方法
let selectMatchFile = (file:any) =>{
let data = {
generatePictureUrl:file.pictureUrl,
price:file.price,
productId:file.productId,
}
formState.value.assortmentList[addMatchIndex].push(data)
}
//增加搭配
let addLook = () =>{
let formStateNew:any = productDetail.value
let data = [{
generatePictureUrl:formStateNew.pictureUrl,
price:formStateNew.price,
productId:formStateNew.id,
}]
formState.value.assortmentList.push(data)
assortmentShowMore.value = true
nextTick(()=>{
document.getElementsByClassName('edit_modal')[0].scrollTo(0, document.getElementsByClassName('edit_modal')[0].scrollHeight)
})
}
let assortmentListShowMore = () =>{
assortmentShowMore.value = !assortmentShowMore.value
}
onMounted(() => {
uploadUrl.value = getUploadUrl()
token.value = getCookie('token') || ''
})
return {
formRef,
submitLoading,
addStoreModalComponent,
uploadUrl,
upload,
token,
fileList,
editProductModal,
columns,
productDetail,
productPicSeletcModal,
ProductMatchModalComponent,
formState,
productCategoryList,
productAttributeList,
rules,
getPopupContainer,
closeProduct,
showModal,
openUploadModal,
deleteSingleAllMatch,
deleteSignleMatchImg,
addProductMatch,
submitStore,
productCategoryChange,
fileUploadChange,
addStore,
storeChange,
submitProduct,
selectMatchFile,
addLook,
assortmentShowMore,
assortmentListShowMore
}
},
});
</script>
<style lang="less" scoped>
.add_product_modal_component{
.product_master_diagram{
width: 400px;
height: 400px;
margin: 10px auto;
display: flex;
align-items: center;
justify-content: center;
.product_master_img{
max-width: 100%;
max-height: 100%;
}
}
.store_input{
text-align: center;
}
.attribute_block{
background: #F7F8FC;
border: 1px solid #DFDFDF;
padding-top: 30px;
.attribute_block_item{
padding: 0 22px;
display: inline-block;
margin-bottom: 30px;
width: 50%;
.attribute_block_item_content{
display: flex;
align-items: center;
.attribute_item_title{
width: 120px;
margin-right: 20px;
}
.attribute_item_select{
max-width: 275px;
width: 250px;
}
}
}
}
.null_data_block{
background: #F7F8FC;
border: 1px solid #DFDFDF;
padding: 30px 0;
text-align: center;
.null_data_img{
width: 130px;
}
}
.product_match_block{
margin-top: 20px;
.product_match_item{
background: #F7F8FC;
border: 1px solid #DFDFDF;
padding: 0 16px 26px;
margin-bottom: 20px;
.product_match_item_header{
height: 83px;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 18px;
color: #030303;
}
.product_match_item_content{
display: flex;
.match_item_img_block{
display: flex;
align-items: center;
justify-content: center;
width: 120px;
height: 90px;
margin-right: 20px;
position: relative;
border: 1px solid #DFDFDF;
img{
max-width: 100%;
max-height: 100%;
}
.delete_img_block{
position: absolute;
right: -10px;
top: -10px;
width: 20px;
height: 20px;
background: #000000;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
.delete_icon{
font-size: 16px;
color: #fff;
}
}
&:last-child{
margin-right: 0;
}
}
}
}
}
.form_edit_table{
margin: 20px 0;
}
.margin_bottom_10{
margin-bottom: 10px;
}
.show_more_button{
margin: 20px auto;
display: block;
}
}
</style>
<style lang="less">
.edit_product_pic{
width: 100%;
height: 100%;
.ant-upload-list{
width: 100%;
height: 100%;
.ant-upload-select-picture-card{
width: 100%;
height: 100%;
background: #fff;
border: none;
}
}
}
</style>

View File

@@ -0,0 +1,529 @@
<template>
<a-modal class="product_detail_modal_component"
v-model:visible="productDetailModal"
:footer="null"
title="商品详情"
width="1150px"
:maskClosable="false"
:centered="true"
>
<div class="productDetail_modal">
<div class="productDetail_modal_cotent">
<div class="productDetail_modal_cotent_center">
<div class="detail_item_block">
<div class="detail_item_title_block">
<span class="icon iconfont icon-a-gengduocaidangongneng detail_icon"></span>
<div>Product image</div>
</div>
<div class="product_master_diagram">
<img class="product_master_img" :src="formState.pictureUrl">
</div>
</div>
<div class="detail_item_block">
<div class="detail_item_title_block">
<span class="icon iconfont icon-a-gengduocaidangongneng detail_icon"></span>
<div>Product label(s)</div>
</div>
<div class="detail_item_content">
<div class="productCategory_block">
<div class="produce_label_item" :style="{background:colorMap[label.type]}" v-for="label in productLabelList" :key="label.id">{{label.name}}</div>
</div>
</div>
</div>
<div class="detail_item_block">
<div class="detail_item_title_block">
<span class="icon iconfont icon-a-gengduocaidangongneng detail_icon"></span>
<div>Pirce</div>
</div>
<div class="detail_item_content">
<div class="productCategory_block">${{formState.price}}</div>
</div>
</div>
<div class="detail_item_block">
<div class="detail_item_title_block">
<span class="icon iconfont icon-a-gengduocaidangongneng detail_icon"></span>
<div>Inventory</div>
</div>
<div class="detail_item_content">
<a-table class="form_width_100" :columns="columns" :data-source="formState.storeList" bordered
:pagination="false" :scroll="{y: 400 }">
</a-table>
</div>
</div>
<div class="detail_item_block">
<div class="detail_item_title_block">
<span class="icon iconfont icon-a-gengduocaidangongneng detail_icon"></span>
<div>Label attributes</div>
</div>
<div class="detail_item_sec_title">类目</div>
<div class="detail_item_content">
<div class="productCategory_block">{{formState.productCategory}}</div>
</div>
<div>
<div class="detail_item_sec_title sec_title_margin">Attribute(s)</div>
<div class="detail_item_content">
<div class="attribute_block" v-if="formState.productAttribute?.length">
<div class="attribute_block_item" v-for="attr in formState.productAttribute" :key="attr.value">
<div class="attribute_block_item_content">
<div class="attribute_item_title">{{attr.labelType}}</div>
<div class="attribute_item_value" :title="attr.attributeValueList.join(',')">{{attr.attributeValueList.join(',')}}</div>
</div>
</div>
</div>
<div v-else class="null_data_block">
<img class="null_data_img" src="@/assets/images/null_img.png">
</div>
</div>
</div>
</div>
<div class="detail_item_block">
<div class="detail_item_title_block">
<span class="icon iconfont icon-a-gengduocaidangongneng detail_icon"></span>
<div>Mix and Match(es)</div>
</div>
<div class="detail_item_content">
<div class="product_match_block">
<div class="product_match_item" v-for="(match,index) in formState.assortmentList" :key="match.title" v-show="index<5 || assortmentShowMore">
<div class="product_match_item_header">
<div>Look{{index+1}}</div>
</div>
<div class="product_match_item_content">
<div class="match_item_img_content" v-for="img in match" :key="img">
<div class="match_item_img_block">
<img :src="img.generatePictureUrl">
</div>
<div class="product_price">${{img.price}}</div>
</div>
</div>
</div>
</div>
<a-button class="primary_button show_more_button" type="primary" @click="assortmentListShowMore()" v-show="formState.assortmentList.length>5">{{assortmentShowMore?'Hide':'See More'}}</a-button>
</div>
</div>
</div>
</div>
<div class="loading_marking" v-show="loading">
<a-spin size="large" />
</div>
</div>
</a-modal>
</template>
<script lang="ts">
import { defineComponent, h,reactive,ref,onMounted,toRefs } from 'vue'
import { message, Upload } from 'ant-design-vue';
import filterComponent from '@/component/filterComponent.vue'
import { Https } from "@/tool/https";
export default defineComponent({
name:'productDetailModal',
setup(){
let loading = ref(false)
let productDetailModal = ref(false)
let productDetail:any = ref([])
let formState:any = ref({
productPic:'',
productLabel:[],
inventoryData:[],
storeList:[],
storeNameList:[],
productCategory:'',
productAttribute:[],
assortmentList:[]
});
let assortmentShowMore = ref(false)
let productCategoryList:any = ref([])
let productAttributeList = ref([])
let columns = reactive([
{ title: 'Store', align:'center', ellipsis: true, dataIndex: 'storeName', key: 'storeName',width:"200px" },
{
title: 'Inventory',
align:'center',
ellipsis: true,
dataIndex: 'inventory',
key: 'inventory',
children: [
{title: 'XS',dataIndex: 'XS',key: 'XS', align:'center', },
{title: 'S',dataIndex: 'S',key: 'S', align:'center',},
{title: 'M',dataIndex: 'M',key: 'M', align:'center',},
{title: 'L',dataIndex: 'L',key: 'L', align:'center',},
{title: 'XL',dataIndex: 'XL',key: 'XL', align:'center',},
{title: 'XXL',dataIndex: 'XXL',key: 'XXL', align:'center',},
],
},
])
let colorMap = ref({
CUSTOM:'#798BD9',
NEW_PRODUCT:'#FFC384',
SALE:'#79D97C',
})
let productLabelList:any = ref([])
let getProductLabelList = () =>{
Https.axiosPost(Https.httpUrls.queryProductLabel).then(
(rv: any) => {
if (rv) {
productLabelList.value = rv.filter((v:any) => formState.value.productLabel.indexOf(v.id) > -1)
}
loading.value = false
}
).catch(res=>{
loading.value = false
});
}
let getProductDetail = (id:any) =>{
return new Promise((resolve:any,rj:any)=>{
Https.axiosGet(Https.httpUrls.productDetail+'?id='+id).then(
(rv: any) => {
if (rv) {
let store = getStoreInfo(rv.storeInfoList)
formState.value = {
pictureUrl:rv.pictureUrl,
productLabel:rv.productLabelInfo.map((v:any)=>v.id),
price:rv.price,
productCategory:rv.attributeItemInfo.labelItem,
productAttribute:rv.attributeItemInfo.attributeTypeList,
storeList:store.storeList,
storeNameList:store.storeNameList,
assortmentList:rv.assortmentList,
}
productDetail.value = rv
resolve(rv)
}
loading.value = false
}
).catch(res=>{
loading.value = false
});
})
}
let getStoreInfo = (storeInfo:any) =>{
let storeList = []
let storeNameList:any = []
for(let item of storeInfo){
let sizeData:any = {}
for(let size of item.stock){
sizeData[size.size] = size.num
}
let data = {
storeName:item.storeName,
storeId:item.storeId,
...sizeData,
}
storeList.push(data)
}
storeInfo.forEach((item:any) => {
item.stock.forEach((stockItem:any) => {
storeNameList.push(stockItem.size);
});
});
// 排序并去重
storeNameList = storeNameList.sort().filter((value:any, index:any, array:any) => {
return array.indexOf(value) === index;
});
let children:any = []
storeNameList.forEach((item:any) => {
children.push({
title:item,
dataIndex:item,
key:item,
align:'center'
})
});
columns[1].children = children
return {storeList,storeNameList}
}
let openProductDetailModal = (productId:any) =>{
formState.value = {
productPic:'',
productLabel:[],
inventoryData:[],
storeList:[],
storeNameList:[],
productCategory:'',
productAttribute:[],
assortmentList:[]
}
productDetailModal.value= true
loading.value = true
assortmentShowMore.value = false
getProductDetail(productId).then(rv=>{
getProductLabelList()
})
}
let assortmentListShowMore = () =>{
assortmentShowMore.value = !assortmentShowMore.value
}
return {
loading,
productDetailModal,
columns,
formState,
assortmentShowMore,
productCategoryList,
productAttributeList,
colorMap,
productLabelList,
openProductDetailModal,
assortmentListShowMore
}
},
})
</script>
<style lang="less" scoped>
.product_detail_modal_component{
.productDetail_modal{
height: 100%;
position: relative;
.productDetail_modal_cotent{
width: 100%;
height: 100%;
background: #fff;
.productDetail_modal_cotent_center{
width: 1000px;
padding: 20px 0 30px;
margin: 0 auto;
height: 100%;
}
.detail_item_block{
margin-bottom: 15px;
.detail_icon{
color: #80B8F8;
font-size: 24px;
margin-right: 14px;
}
.detail_item_title_block{
display: flex;
align-items: center;
font-size: 16px;
font-family: Adobe Heiti Std;
font-weight: normal;
color: #030303;
margin-bottom: 10px;
}
.detail_item_content{
padding-left: 10px;
width: 100%;
position: relative;
}
.produce_label_item{
padding: 0 11px;
margin: 9px 27px 9px 0;
height: 27px;
line-height: 27px;
box-sizing: border-box;
border-radius: 4px;
font-size: 16px;
color: #fff;
display: inline-block;
vertical-align: top;
}
.productCategory_block{
width: 100%;
line-height: 46px;
padding-left: 20px;
font-size: 18px;
color: #030303;
height: 46px;
border: 1px solid #DFDFDF;
}
.detail_item_sec_title{
padding-left: 38px;
font-size: 16px;
font-family: Adobe Heiti Std;
font-weight: normal;
color: #030303;
margin-bottom: 10px;
}
.sec_title_margin{
margin: 10px 0;
}
.attribute_block{
background: #F7F8FC;
border: 1px solid #DFDFDF;
padding-top: 30px;
.attribute_block_item{
padding: 0 22px;
display: inline-block;
margin-bottom: 30px;
width: 50%;
.attribute_block_item_content{
display: flex;
align-items: center;
.attribute_item_title{
width: 120px;
margin-right: 20px;
}
.attribute_item_value{
width: 200px;
height: 36px;
line-height: 36px;
border: 1px solid #DFDFDF;
border-radius: 4px;
font-size: 16px;
padding-left: 18px;
color: #030303;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
}
}
.null_data_block{
background: #F7F8FC;
border: 1px solid #DFDFDF;
padding: 30px 0;
text-align: center;
.null_data_img{
width: 130px;
}
}
.product_match_block{
.product_match_item{
background: #F7F8FC;
border: 1px solid #DFDFDF;
padding: 0 16px 26px;
margin-bottom: 20px;
.product_match_item_header{
height: 83px;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 18px;
color: #030303;
}
.product_match_item_content{
display: flex;
.match_item_img_content{
margin-right: 20px;
width: 160px;
}
.match_item_img_block{
display: flex;
align-items: center;
justify-content: center;
width: 160px;
height: 100px;
position: relative;
border: 1px solid #DFDFDF;
cursor: pointer;
img{
max-width: 100%;
max-height: 100%;
}
&:last-child{
margin-right: 0;
}
}
.product_price{
text-align: center;
margin-top: 5px;
color: #030303;
}
}
}
}
.show_more_button{
margin: 20px auto;
display: block;
}
}
.form_width_100{
width: 100%;
}
.product_master_diagram{
width: 400px;
height: 400px;
margin: 30px auto;
display: flex;
align-items: center;
justify-content: center;
.product_master_img{
max-width: 100%;
max-height: 100%;
}
}
}
.loading_marking{
position: absolute;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.3);
left: 0;
top: 0;
display: flex;
align-items: center;
justify-content: center;
}
}
}
</style>

View File

@@ -0,0 +1,341 @@
<template>
<a-modal class="add_match_modal_component"
v-model:visible="productMatchModal"
:footer="null"
title="商品搭配选择"
width="1150px"
:maskClosable="false"
:centered="true"
>
<div class="upload_modal_body">
<div class="filter_content">
<div class="filter_content_block">
<filterComponent class="filter_component_style" :title="'Product Type'" :titleStyle="titleStyle" :slotStyle="slotStyle"><a-select v-model:value="labelItem" size="large" style="width:200px" :options="productCategoryList" placeholder="Please select" allowClear @change="productTypeChange"></a-select></filterComponent>
<div class="filter_component_style item_subtype_content">
<div class="item_subtype_button" @click="changeItemFilter">
<span>Item Subtype</span>
<span :class="['icon','iconfont','item_subtype_button_icon','icon-xialajiantouxiao', showItemFilter?'turn_round_icon':'']"></span>
</div>
</div>
<div v-show="showItemFilter">
<filterComponent v-for="item in productAttributeList" :key="item.title" class="filter_component_style" :title="item.labelType" :titleStyle="titleStyle" :slotStyle="slotStyle">
<a-select v-model:value="item.value" size="large" style="width:200px" :options="item.attributeValueList" placeholder="Please select" allowClear></a-select>
</filterComponent>
</div>
</div>
<div class="filter_content_button_list">
<a-button class="primary_button btn-margin-r-20" type="primary" size="large" @click="searchList()">Search</a-button>
<a-button class="default_button" size="large">Reset</a-button>
</div>
</div>
<div class="modal_body_content">
<div class="upload_img_body scroll_style" v-show="imageList.length">
<div class="upload_file_item" v-for="(file) in imageList" :key="file.id">
<div :class="['upload_file_item_content']" @click="selectFile(file)">
<!-- <img v-lazy="img.url" class="upload_img"> -->
<img :src="file.pictureUrl" class="upload_img">
</div>
<div class="product_price">${{file.price}}</div>
</div>
</div>
<div class="upload_img_body null_img_block" v-show="!imageList.length">
<img src="@/assets/images/null_img.png">
</div>
<div class="img_mark_loading" v-show="tableLoading">
<a-spin size="large" />
</div>
<a-pagination class="pagination_content" show-quick-jumper v-model:current="currentPage" :page-size-options="pageSizeOptions" show-size-changer :page-size="pageSize" :total="total" @change="pageChange" @showSizeChange="onShowSizeChange"/>
</div>
</div>
</a-modal>
</template>
<script lang="ts">
import { defineComponent, h,reactive,ref,onMounted,toRefs } from 'vue'
import { message, Upload } from 'ant-design-vue';
import filterComponent from '@/component/filterComponent.vue'
import { Https } from "@/tool/https";
export default defineComponent({
name:'ProductMatchModal',
components:{filterComponent},
props:{
productCategoryList:{
type:Array,
}
},
setup(props,{emit}){
let productMatchModal = ref(false)
let filter = reactive({
labelItem:[],
labelTypeMap:{},
})
let tableLoading = ref(false)
let imageList:any = ref([])
let currentPage = ref(1)
let total = ref(0)
let pageSize = ref(10)
let pageSizeOptions = ref<string[]>(['10', '20', '30', '40', '50']);
let titleStyle = ref({textAlign:'left',minWidth:'120px',textOverflow:'ellipsis',width:'120px',overflow:'hidden'})
let slotStyle = ref({width:'auto'})
let showItemFilter = ref(false)
let subTypeList:any = ref([])
let productAttributeList:any = ref([])
let assortmentIdList:any = []
let openMatchModal = (idList:any) =>{
productMatchModal.value = true
productAttributeList.value = []
filter.labelItem = []
filter.labelTypeMap = {}
currentPage.value = 1
imageList.value = []
assortmentIdList = idList
getProductAssortemlist()
}
let pageChange = (e:any) =>{
currentPage.value = e
getProductAssortemlist()
}
let onShowSizeChange = (current: number, size: number) =>{
pageSize.value = size
getProductAssortemlist()
}
let productTypeChange = (value:any,selectedOptions:any) => {
let attributeList = selectedOptions ? selectedOptions.attributeTypeList :[]
productAttributeList.value = JSON.parse(JSON.stringify(attributeList))
}
let changeItemFilter = () =>{
showItemFilter.value = !showItemFilter.value
}
let selectFile = (file:any) =>{
emit('selectMatchFile',file)
productMatchModal.value = false
}
let getLabelTypeMap = () =>{
let labelTypeMap:any = {}
for(let item of productAttributeList.value){
if(item.value && item.value.length){
labelTypeMap[item.labelType] = [item.value]
}
}
return labelTypeMap
}
let searchList = () =>{
currentPage.value = 1
getProductAssortemlist()
}
let getProductAssortemlist = () =>{
let data = {
assortmentIdList:assortmentIdList,
labelItem:filter.labelItem && filter.labelItem.length ? filter.labelItem : '',
labelTypeMap:getLabelTypeMap(),
page:currentPage.value,
size:pageSize.value
}
tableLoading.value = true
Https.axiosPost(Https.httpUrls.queryProductAssortmentPage, data).then(
(rv: any) => {
if (rv) {
tableLoading.value = false
imageList.value = rv.content
total.value = rv.total
}
}
);
}
onMounted(()=>{
})
return {
...toRefs(filter),
productMatchModal,
tableLoading,
imageList,
currentPage,
total,
pageSize,
pageSizeOptions,
titleStyle,
slotStyle,
showItemFilter,
subTypeList,
productAttributeList,
openMatchModal,
pageChange,
onShowSizeChange,
productTypeChange,
changeItemFilter,
selectFile,
searchList,
}
},
})
</script>
<style lang="less" scoped>
.add_match_modal_component{
.upload_modal_body{
.filter_content{
.filter_content_block{
.filter_component_style:nth-child(3n){
margin-right: 0;
}
.item_subtype_content{
width: 335px;
display: inline-block;
overflow: hidden;
vertical-align: top;
.item_subtype_button{
width: 160px;
height: 46px;
border: 1px solid #030303;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
font-family: Roboto;
font-weight: 400;
color: #030303;
cursor: pointer;
float: right;
.item_subtype_button_icon{
margin-left: 15px;
color: #030303;
font-size: 16px;
}
.turn_round_icon{
-moz-transform:rotate(180deg);
-webkit-transform:rotate(180deg);
transform: rotate(180deg);
animation-direction: 0.5s;
}
}
}
}
.filter_content_button_list{
display: flex;
align-items: center;
}
}
.modal_body_content{
margin-top: 40px;
padding: 25px;
background: #F7F8FC;
border: 1px solid #DFDFDF;
height: 550px;
position: relative;
.upload_img_body{
height: calc(100% - 30px);
overflow-y: auto;
margin-bottom: 10px;
}
.null_img_block{
display: flex;
align-items: center;
justify-content: center;
}
.img_mark_loading{
width: 100%;
height: 100%;
position: absolute;
left: 0;
top: 0;
background: rgba(0,0,0,0.2);
display: flex;
align-items: center;
justify-content: center;
z-index: 999;
}
.upload_file_item{
margin: 0 50px 30px 0;
display: inline-block;
width: 165px;
height: 165px;
border: 1px solid #F5F5F5;
vertical-align: top;
background: rgba(0,0,0,0.2);
&:nth-child(5n){
margin-right: 0;
}
.upload_file_item_content{
display: flex;
align-items: center;
justify-content: center;
height: 100%;
width: 100%;
position: relative;
cursor: pointer;
&.select_file_block{
border:solid 1px #000;
}
.upload_img{
max-height: 100%;
max-width: 100%;
}
}
.product_price{
text-align: center;
margin-top: 5px;
color: #030303;
}
}
}
}
.pagination_content{
text-align: center;
}
}
</style>

View File

@@ -0,0 +1,318 @@
<template>
<a-modal class="add_product_modal_component"
v-model:visible="addProductPicModal"
:footer="null"
title="商品图片上传"
width="1150px"
:maskClosable="false"
:destroyOnClose="true"
:centered="true"
@cancel="closeModal"
>
<div class="upload_modal_body">
<div class="upload_img_body scroll_style" >
<div class="upload_item">
<div class="upload_file_item" v-for="(file, index) in fileList" :key="file">
<div class="upload_file_item_content" v-show="file?.status === 'uploading'">
<a-spin :indicator="indicator" tip="Uploading..."/>
</div>
<div class="upload_file_item_content" v-show="file?.status === 'done'">
<img v-lazy="file?.pictureUrl" class="upload_img">
<div class="delete_file_block" @click="deleteFile(index)">
<span class="icon iconfont icon-shanchu"></span>
</div>
</div>
</div>
<div class="upload_file_item upload_component" v-show="fileList.length < 100">
<a-upload
:action="uploadUrl + '/api/product/uploadFileSingle'"
class="upload_component_button"
list-type="picture-card"
:data="{
...upload
}"
:headers="{Authorization:token}"
v-model:file-list="fileList"
:before-upload="beforeUpload"
multiple
:maxCount="100"
accept=".jpg,.png,.jpeg,.bmp"
@change="(file)=>fileUploadChange(file)"
>
<div class="upload_tip_block" v-show="fileList.length < 100">
<span class="icon iconfont add_pic_button_icon icon-tianjiatupian_huaban"></span>
</div>
</a-upload>
</div>
</div>
</div>
<div class="upload_max_tip">
<WarningFilled class="icon-zhuyi"/>
<span>Maximum 100 images can be uploaded, Maximum 2M per image</span>
</div>
</div>
<div class="modal_button_list">
<a-button class="primary_button" type="primary" size="large" @click="closeModal()">Submit</a-button>
</div>
</a-modal>
</template>
<script lang="ts">
import { defineComponent, h,reactive,ref,onMounted} from 'vue'
import { LoadingOutlined,WarningFilled } from '@ant-design/icons-vue';
import {getCookie} from '@/tool/cookie'
import {getUploadUrl} from '@/tool/util'
import { message, Upload } from 'ant-design-vue';
export default defineComponent({
name:'ProductPicUpload',
components:{WarningFilled},
emits: ['uploadPicChange'],
setup(props,{ emit }){
let addProductPicModal = ref(false)
let fileList:any = ref([])
let templateModal:any = ref(false)
let templateFileList:any = ref([])
let upload:any = reactive({
timeZone:Intl.DateTimeFormat().resolvedOptions().timeZone,
})
let beforeUploadIndex:any = 0
let successFileList:any = ref([])
let token:any = ref('')
let indicator :any = h(LoadingOutlined, {
style: {
fontSize: '24px',
},
spin: true,
})
let uploadUrl:any = ref('')
let fileUploadChange = (data:any) =>{
let file = data.file
if(file.status === 'done'){
let res = file.response
if(res.data){
file.pictureUrl = res.data.pictureUrl
file.md5 = res.data.md5
file.resData = res.data
successFileList.value = fileList.value.filter((v:any)=> v.status === 'done')
emit('uploadPicChange', {fileList:successFileList.value})
}else{
let index = -1
fileList.value.forEach((ele:any,index1:any) => {
if(file.uid === ele.uid){
index = index1
}
});
if(index > -1){
fileList.value.splice(index, 1)
}
message.error(file.name + ' ' + res.errMsg)
}
}else if(file.status === 'error'){
let index = -1
fileList.value.forEach((ele:any,index1:any) => {
if(file.uid === ele.uid){
index = index1
}
});
if(index > -1){
fileList.value.splice(index, 1)
}
message.error(file.name + 'upload failed')
}
}
let beforeUpload = (file:any,fileList:any) => {
const isJpgOrPng = file.type === 'image/jpeg' || file.type === 'image/png' || file.type === 'image/jpg' || file.type === 'image/bmp';
beforeUploadIndex = beforeUploadIndex +1
console.log(file, 66)
if(fileList.length > 10 && beforeUploadIndex == fileList.length){
message.error('You can only upload 10 pictures at a time!');
}
if (!isJpgOrPng) {
message.error('You can only upload Image file!');
}
const isLt2M = file.size / 1024 / 1024 < 5;
if (!isLt2M) {
message.error('Image must smaller than 5MB!');
}
if(beforeUploadIndex == fileList.length) {
beforeUploadIndex = 0
}
return (isJpgOrPng && isLt2M && fileList.length <= 10) || Upload.LIST_IGNORE;
}
let deleteFile = (index:any) =>{
fileList.value.splice(index,1)
successFileList.value = fileList.value
emit('uploadPicChange', {fileList:successFileList.value})
}
let openPictureModal = () =>{
addProductPicModal.value = true
}
let closeModal = () =>{
addProductPicModal.value = false
}
let clearPicData = () =>{
fileList.value = []
successFileList.value =[]
}
onMounted(()=>{
token.value = getCookie('token') || '',
uploadUrl.value = getUploadUrl()
})
return {
addProductPicModal,
fileList,
templateModal,
templateFileList,
upload,
successFileList,
indicator,
token,
uploadUrl,
fileUploadChange,
beforeUpload,
deleteFile,
openPictureModal,
closeModal,
clearPicData
}
},
})
</script>
<style lang="less" scoped>
.add_product_modal_component{
.upload_modal_body{
height: 600px;
.upload_img_body{
height: calc(100% - 30px);
overflow-y: auto;
margin-bottom: 10px;
}
.upload_file_item{
margin: 0 17px 17px 0;
display: inline-block;
width: 165px;
height: 165px;
border: 1px solid #F5F5F5;
vertical-align: top;
&.upload_component{
background: #FFFFFF;
.upload_tip_block{
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
.add_pic_button_icon{
font-size: 44px;
color: #C6CBD3;
}
}
}
.upload_file_item_content{
display: flex;
align-items: center;
justify-content: center;
height: 100%;
width: 100%;
position: relative;
&:hover .delete_file_block{
display: block;
}
.upload_img{
display: block;
max-height: 100%;
max-width: 100%;
}
.delete_file_block{
display: none;
width: 36px;
border-radius: 50%;
cursor: pointer;
height: 36px;
background: #000000;
font-size: 16px;
color: #FFFFFF;
line-height: 36px;
text-align: center;
position: absolute;
right: 10px;
top: 10px;
}
}
}
.upload_max_tip{
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
color: #030303;
margin-bottom: 30px;
.icon-zhuyi{
font-size: 16px;
margin-right: 8px;
}
}
}
.modal_button_list{
display: flex;
justify-content: flex-end;
}
}
</style>
<style lang="less">
.upload_component_button{
width: 165px;
height: 165px;
.ant-upload-list{
width: 100%;
height: 100%;
.ant-upload-select-picture-card{
width: 100%;
height: 100%;
}
}
}
</style>

18
src/main.ts 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 './assets/iconfont/iconfont.css'
import './assets/iconfont2/iconfont.css'
import './assets/iconfont/iconfont.js'
import 'ant-design-vue/dist/antd.css';
import Antd from 'ant-design-vue';
import './assets/style/style.less'
import VueLazyload from "vue-lazyload";
let loadingParam = {
loading: require('./assets/images/loading.gif'),
attempt: 1
}
createApp(App).use(store).use(router).use(Antd).use(VueLazyload,loadingParam).mount('#app')

134
src/router/index.ts Normal file
View File

@@ -0,0 +1,134 @@
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
import { defineAsyncComponent } from 'vue'
const _import = (path : string) => defineAsyncComponent(() => import(`../views/${path}.vue`));
const _import_custom = (path : string) => defineAsyncComponent(() => import(`../views/${path}`));
import {getCookie} from '@/tool/cookie'
const routes: Array<RouteRecordRaw> = [
{
path: "/",
// redirect重定向
redirect: "/login"
},
{
path: '/login',
name: 'login',
component: _import('LoginPage')
},
{
path: '/noPermission',
name: 'noPermission',
component: _import('NoPermissionPage')
},
{
path: '/home',
name: 'home',
component: _import('HomeView'),
children:[
{
path:'worktable',
name:'worktable',
component: _import_custom('childView/worktable.vue'),
},
{
path:'storemanage',
name:'storeManage',
component: _import_custom('childView/storeManage.vue'),
},
{
path:'labelmanage',
name:'labelManage',
component: _import_custom('childView/labelManage.vue'),
},
{
path:'productmanage',
name:'productManage',
component: _import_custom('childView/productManage.vue'),
},
{
path:'productDetail',
name:'productDetail',
component: _import_custom('childView/productDetail.vue'),
},
{
path:'usermanage',
name:'userManage',
component: _import_custom('childView/systemSetting/userManage.vue'),
},
{
path:'rolemanage',
name:'roleManage',
component: _import_custom('childView/systemSetting/roleManage.vue'),
},
{
path:'exportExcil',
name:'exportExcil',
component: _import_custom('childView/exportExcil.vue'),
},
// {
// path:'excil1',
// name:'excil1',
// component: _import_custom('childView/exportExcil/userManage.vue'),
// },
// {
// path:'excil2',
// name:'excil2',
// component: _import_custom('childView/exportExcil/roleManage.vue'),
// },
]
},
]
const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes
})
router.beforeEach((to, from, next) =>{
let routeRoleMap:any = sessionStorage.getItem('menuList')
routeRoleMap = JSON.parse(routeRoleMap) || []
let routeList = ['/login','/home/productDetail','/noPermission']
if(!getCookie('token')){
next()
}
if(routeList.indexOf(to.path) > -1){
next()
}else{
let status = false
let allStatus = false
for(let item of routeRoleMap){
allStatus = item.isShow
if(to.path === item.route && item.isShow){
status = true
break
}
if(item.children && item.children.length){
for(let child of item.children){
allStatus = item.isShow
if(to.path === child.route && child.isShow){
status = true
break
}
}
}
if(status) {
break
}
}
if(!allStatus){
next('/login')
}
if(status){
next()
}else{
next('/noPermission')
}
}
})
export default router

View File

@@ -0,0 +1,29 @@
import {Module} from 'vuex'
import {RootState} from '../index'
interface menuData{
menuList:any
}
const homeMenu : Module<menuData,RootState> = {
state:{
menuList:[],
},
mutations:{
setMenuList(state,data){
state.menuList = data
},
},
actions:{
}
}
export default homeMenu

19
src/store/index.ts Normal file
View File

@@ -0,0 +1,19 @@
import { createStore } from 'vuex'
import homeMenuModule from './homeMenu/homeMenu'
export interface RootState{
}
export default createStore<RootState>({
state: {
},
getters: {
},
mutations: {
},
actions: {
},
modules: {
homeMenuModule,
}
})

30
src/tool/cookie.js Normal file
View File

@@ -0,0 +1,30 @@
const setCookie = (name,value) => {
var Days = 30;
var exp = new Date();
exp.setTime(exp.getTime() + Days*24*60*60*30);
document.cookie = name + "="+ escape (value) + ";expires=" + exp.toGMTString();
}
const getCookie = (name) => {
var arr,reg=new RegExp("(^| )"+name+"=([^;]*)(;|$)");
if(arr=document.cookie.match(reg))
return unescape(arr[2]);
else
return null;
}
function WriteCookie(name) {
var now = new Date();
now.setMonth( now.getMonth() - 1 );
cookievalue = escape(document.myform.customer.value) + ";"
document.cookie = name + '=' + cookievalue;
document.cookie = "expires=" + now.toUTCString() + ";"
document.write("Setting Cookies : " + "name=" + cookievalue );
}
export {
setCookie,
getCookie,
WriteCookie
}

146
src/tool/https.js Normal file
View File

@@ -0,0 +1,146 @@
import axios from 'axios'
// import qs from 'qs'
// import message from '@/components/public/message/src'
import router from '@/router/index'
import {getCookie} from '@/tool/cookie'
// import cookie from '@/tools/cookie.js'
axios.defaults.timeout = 60000 * 60; //响应时间
axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded;charset=UTF-8'; //配置请求头
axios.defaults.withCredentials = true; //跨域携带cookie
import { message } from 'ant-design-vue';
axios.defaults.baseURL = process.env.VUE_APP_BASE_URL; //配置接口地址
//POST传参序列化(添加请求拦截器)
axios.interceptors.request.use((config) => {
//在发送请求之前做某件事
if(config.method === 'post' || config.method === 'put' || config.method === 'delete'){
// config.data = qs.stringify(config.data);
// config.data = JSON.stringify(config.data);
}
config.headers.Authorization = getCookie('token');
return config;
},(error) =>{
return Promise.reject(error);
});
//返回状态判断(添加响应拦截器)
axios.interceptors.response.use((res) =>{
if (res.data) {
if (res.data.errCode === 0) {
return Promise.resolve(res.data.data);
} else if(!res.data.errMsg){
let obj = {
data:res.data,
type:res.headers['content-type'],
name:res.headers['content-disposition'],
}
return Promise.resolve(obj);
}else{
message.error(res.data.errMsg)
return Promise.reject(res.data);
}
} else{
message.error(res.data.errMsg)
return Promise.reject(res.data);
}
}, function(error) {
if(error?.response?.status === 401){
router.replace('/login')
return Promise.reject()
}
let data_new = error?.response?.data
message.error(data_new?.errMsg || 'Error: server exception')
return Promise.reject(data_new);
});
export const Https = {
httpUrls: {
interfaceUrl: '',
accountLogin:'/api/account/login', //登入
accountLogout:'/api/account/logout',//登出
queryStorePage:'/api/store/queryStorePage',//门店分页列表
sotreSaveOrEdit:'/api/store/saveOrEdit',//新增或编辑门店
storeDelete:'/api/store/delete',//删除门店
labelQueryStorePage:'/api/label/queryStorePage',//标签分页列表
labelSaveOrEdit:'/api/label/saveOrEdit', //新增或编辑标签
labelDelete:'/api/label/delete',//删除标签
accountQueryUserPage:'/api/account/queryUserPage',//用户分页列表
storeQueryAll:'/api/store/queryAll',//下拉-查询所有店铺
roleQueryRolePage:'/api/role/queryRolePage', //角色分页列表
roleQueryAll:'/api/role/queryAll', // 下拉-查询所有角色
accountSaveOrEdit:'/api/account/saveOrEdit',//添加或编辑账号
accountEnable:'/api/account/enable', // 禁用-停用账户
accountDelete:'/api/account/delete',//删除账户
roleQueryPermissionList:'/api/role/queryPermissionList',//查询权限列表
roleSaveOrEdit:'/api/role/saveOrEdit', //新增或编辑角色
roleDelete:'/api/role/delete', //删除角色
queryProductPage:'/api/product/queryProductPage',//商品分页列表
queryProductLabel:'/api/label/queryProductLabel',//下拉-查询所有商品标签
queryProductAssortmentPage:'/api/product/queryProductAssortmentPage',//商品搭配分页列表
attributeQueryAll:'/api/attribute/queryAll',//下拉-查询所有属性值
productConfirmUpload:'/api/product/confirmUpload',
countWorkBench:'/api/product/countWorkBench',//首页工作台统计
batchUploadProductRelation:'/api/product/batchUploadProductRelation',//批量上传商品后传对应的关联信息,新增商品
productDelete:'/api/product/delete', //删除商品
countProductUpdateProcess:'/api/product/countProductUpdateProcess', //统计商品批量上传进度
productEdit:'/api/product/edit',//编辑商品
productDetail:'/api/product/detail',//商品详情
doOnSale:'/api/product/doOnSale', //上架-下架商品
queryUsrPermission:'/api/role/queryUsrPermission',//菜单权限
queryProductStore:'/api/store/queryProductStore', //下拉-查询所有商品店铺
exportProduct:'/api/product/exportProduct', //商品导出
miTuExportPage:'/api/miTuExport/miTuExportPage', //获取导出报表列表
miTuExportExport:'/api/miTuExport/export', //商品导出
},
axiosGet(url,config) {
return new Promise((resolve, reject) => {
axios.get(url,config).then(response => {
resolve(response)
}).catch((error) => {
reject(error)
})
});
},
axiosPut(url, data) {
return new Promise((resolve, reject) => {
axios.put(url, data).then(response => {
resolve(response)
}).catch((error) => {
reject(error)
})
});
},
axiosPost(url, data,config) {
return new Promise((resolve, reject) => {
axios.post(url, data,config).then(response => {
resolve(response)
}).catch((error) => {
reject(error)
})
});
},
axiosDelete(url, newData) {
return new Promise((resolve, reject) => {
axios.delete(url,{data:newData}).then(response => {
resolve(response)
}).catch((error) => {
reject(error)
})
});
},
}

197
src/tool/util.js Normal file
View File

@@ -0,0 +1,197 @@
import * as XLSX from 'xlsx';
const isEmail = (email)=>{
let reg = /^([A-Za-z0-9_\-\.])+\@([A-Za-z0-9_\-\.])+\.([A-Za-z]{2,4})$/
let result = reg.test(email)
return result
}
const getUploadUrl = () =>{
let url = process.env.VUE_APP_BASE_URL || ''
return url
}
function dataURLtoBlob(dataurl) {
var arr = dataurl.split(',');
var mime = arr[0].match(/:(.*?);/)[1];
var bstr =atob(arr[1]);
var n = bstr.length;
var u8arr =new Uint8Array(n);
while (n--) {
u8arr[n] = bstr.charCodeAt(n);
}
return new Blob([u8arr], {type: mime });
}
function blobToFile(blob, fileName){
blob.lastModifiedDate =new Date();
blob.name = fileName;
return blob;
}
function dataURLtoFile(dataurl, filename){
var arr = dataurl.split(','), mime = arr[0].match(/:(.*?);/)[1],
bstr = atob(arr[1]), n = bstr.length, u8arr = new Uint8Array(n);
while(n--){
u8arr[n] = bstr.charCodeAt(n);
}
var blob = dataURLtoBlob(dataurl);
return blobToFile(blob, filename);
}
const base64toFile = (dataurl, filename = 'file') => {
let arr = dataurl.split(',')
let mime = arr[0].match(/:(.*?);/)[1]
let suffix = mime.split('/')[1]
let bstr = atob(arr[1])
let n = bstr.length
let u8arr = new Uint8Array(n)
while (n--) {
u8arr[n] = bstr.charCodeAt(n)
}
return new File([u8arr], `${filename}.${suffix}`, {
type: mime
})
}
function rgbToHsv([R, G, B]) {
R /= 255
G /= 255
B /= 255
const max = Math.max(R, G, B)
const min = Math.min(R, G, B)
const range = max - min
let V = max
let S = V === 0 ? 0 : range / V
let H = 0
if (R === V) H = (60 * (G - B)) / range
if (G === V) H = 120 + (60 * (B - R)) / range
if (B === V) H = 240 + (60 * (R - G)) / range
if (range === 0) H = 0
if (H < 0) H += 360
H = (H / 2) * (256 / 180)
S *= 255
V *= 255
H = parseInt(H)
S = parseInt(S)
V = parseInt(V)
return [H, S, V]
}
const formatTime = (timestamp, fmt) =>{
// date = new Date(), fmt = 'MM/dd/yyyy';
let date = new Date();
date.setTime(timestamp * 1000);
if (!fmt) {
formatRule ? (fmt = formatRule) : (fmt = "YYYY-MM-DD hh:mm:ss");
}
// console.log(formatRule)
let o = {
'M+': date.getMonth() + 1, // 月份
'D+': date.getDate(), // 日
'h+': date.getHours(), // 小时
'm+': date.getMinutes(), // 分
's+': date.getSeconds(), // 秒
'q+': Math.floor((date.getMonth() + 3) / 3), // 季度
'S+': date.getMilliseconds(), // 毫秒
'a': date.getHours() > 12
? 'PM'
: 'AM' // 上午还是下午
};
if (/(Y+)/.test(fmt)) {
fmt = fmt.replace(RegExp.$1, (date.getFullYear() + '').substr(4 - RegExp.$1.length));
}
if(/(a)/.test(fmt)&&o['h+']>12){
o['h+'] = o['h+'] - 12
}
for (let k in o) {
if (new RegExp('(' + k + ')').test(fmt)) {
fmt = fmt.replace(RegExp.$1, (RegExp.$1.length == 1)
? (o[k])
: (('00' + o[k]).substr(('' + o[k]).length)));
}
}
return fmt;
}
const startTime = (time) => {
const nowTimeDate = new Date(time)
return nowTimeDate.setHours(0, 0, 0, 0)
}
const endTime = (time)=> {
const nowTimeDate = new Date(time)
return nowTimeDate.setHours(23, 59, 59, 999)
}
const isMoible = () => {
let is_mobile = navigator.userAgent.toLowerCase().match(/(ipad|ipod|iphone|android|coolpad|mmp|smartphone|midp|wap|xoom|symbian|j2me|blackberry|wince)/i) != null;
if(is_mobile){
return true
}else{
return false
}
}
//根据某个字段去重
const unique = (arr, val) => {
const res = new Map()
return arr.filter((item) => !res.has(item[val]) && res.set(item[val], 1))
}
const exportExcil=(data,sheetNames,fileName,configuration)=>{
const workbook = XLSX.utils.book_new();
for (let i = 0; i < data.length; i++) {
let worksheet = XLSX.utils.aoa_to_sheet(data[i]);
// let worksheet = XLSX.utils.json_to_sheet(data[i]);
const colWidths = [];
for (let y = 0; y < data[i].length; y++) {
colWidths.push({ wch: configuration.width });
}
worksheet['!cols'] = colWidths;
const merges = [
{ s: { r: 0, c: 0 }, e: { r: 0, c: configuration.titleCell } },
]
worksheet['!merges'] = merges;
const range = XLSX.utils.decode_range(worksheet['!ref']);
for (let R = range.s.r; R <= range.e.r; ++R) {
for (let C = range.s.c; C <= range.e.c; ++C) {
const cellAddress = { r: R, c: C };
const cellRef = XLSX.utils.encode_cell(cellAddress);
if (!worksheet[cellRef]) continue;
console.log(worksheet[cellRef]);
worksheet[cellRef].s = { alignment: { horizontal: 'center', vertical: 'center',wrapText: true, } }; // 设置文本水平和垂直居中
}
}
XLSX.utils.book_append_sheet(workbook, worksheet, sheetNames[i]);
}
// XLSX.writeFile(workbook, fileName);
XLSX_STYLE.write(workbook, {
bookType: 'xlsx',
bookSST: true,
type: 'binary',
cellStyles: true,
})
}
export{
isEmail,
getUploadUrl,
dataURLtoFile,
blobToFile,
base64toFile,
rgbToHsv,
formatTime,
dataURLtoBlob,
startTime,
endTime,
isMoible,
unique,
exportExcil
}

374
src/views/HomeView.vue Normal file
View File

@@ -0,0 +1,374 @@
<template>
<div class="home_page">
<div class="home_menu_content">
<img class="home_menu_log" src="@/assets/images/home_logo.png">
<a-menu
id="dddddd"
class="menu_list_content"
theme="dark"
v-model:openKeys="openKeys"
v-model:selectedKeys="selectedKeys"
mode="inline"
@click="handleClick"
>
<div v-for="(menu) in showMenuList" :key="menu.key" >
<a-menu-item :key="menu.key" :name="menu.name" :route="menu.route" v-if="!menu.children">
<template #icon><span :class="['icon','iconfont', 'menu_icon', menu.icon]"></span></template>
<span class="menu_title">{{menu.name}}</span>
</a-menu-item>
<a-sub-menu :key="menu.key" v-else>
<template #icon>
<span :class="['icon','iconfont', 'menu_icon', menu.icon]"></span>
</template>
<template #expandIcon><span :class="['icon','iconfont', 'menu_icon', menu.expandIcon]"></span></template>
<template #title><span class="menu_title">{{menu.name}}</span></template>
<div >
<a-menu-item v-for="child in menu.children" :key="child.key" :name="child.name" :route="child.route"><span class="menu_title">{{child.name}}</span></a-menu-item>
</div>
</a-sub-menu>
</div>
</a-menu>
</div>
<div class="home_right_content">
<div class="home_content_header">
<div class="page_name">
<div v-show="!routers.length" class="page_name_title">{{nowPageName}}</div>
<a-breadcrumb v-show="routers.length" :routes="routers">
<template #itemRender="{ route, routes, paths }">
<span v-if="routes.indexOf(route) === routes.length - 1">
{{ route.breadcrumbName }}
</span>
<router-link v-else :to="`${paths.join('/')}`">
{{ route.breadcrumbName }}
</router-link>
</template>
</a-breadcrumb>
</div>
<a-popover v-model:visible="popoverModal" title="" trigger="click">
<template #content>
<div class="select_item" @click="logout()">
<span class="icon iconfont icon-tuichu1"></span><span class="select_item_des">log off</span>
</div>
</template>
<div class="user_info" @click="popoverModal = !popoverModal">
<img class="header_img" src="@/assets/images/header.png"/>
<div class="user_name">{{userInfo.userName}}</div>
<span :class="['icon','iconfont','header_icon','icon-xialajiantouxiao', popoverModal?'turn_round_icon':'']"></span>
</div>
</a-popover>
</div>
<div class="home_content_body">
<router-view/>
</div>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, ref,reactive,toRefs, onMounted,watch,computed } from "vue";
import { useRouter,useRoute } from 'vue-router'
import { Https } from "@/tool/https";
import {setCookie, getCookie, WriteCookie} from '@/tool/cookie'
import { message } from "ant-design-vue";
export default defineComponent({
name:'home',
setup() {
const router = useRouter()
const route:any = useRoute()
let popoverModal = ref(false)
let userInfo:any = ref({})
watch(route, (newValue:any, oldValue) => {
getBreadData(newValue.name)
});
const state:any = reactive({
selectedKeys: ['/home/worktable'],
openKeys: [],
nowPageName:'Workdesk',//当前页面名称
});
let menuList:any = ref([])
let showMenuList:any = computed(()=>{
let menu = []
for(let item of menuList.value){
if(item.isShow && !item.children){
menu.push(item)
}else if(item.isShow && item.children){
let data = {
...item,
children:[]
}
for(let child of item.children){
if(child.isShow){
data.children.push(child)
}
}
menu.push(data)
}
}
return menu
})
let routers:any = ref([])
let handleClick = (event:any) => {
state.selectedKeys = [event.key]
state.nowPageName = event.item.name
router.push({path:event.item.route})
}
let logout = () =>{
let data = {
userId:userInfo.value.userId
}
Https.axiosPost(Https.httpUrls.accountLogout, data).then(
(rv: any) => {
if (rv) {
router.replace('/login')
}
}
);
}
let getBreadData = (routeName:string) =>{
if(routeName == 'productDetail'){
routers.value = [
{
path: '/productmanage',
breadcrumbName: 'Product Management',
},
{
path: '/productDetail',
breadcrumbName: 'Product Detail',
},
]
}else{
routers.value = []
let getRouteInfo = (menuList:any) =>{
for(let item of menuList){
if(!item.children){
if(item.route == route.path){
state.nowPageName = item.name
state.selectedKeys = [item.key]
break
}
}else{
getRouteInfo(item.children)
}
}
}
getRouteInfo(menuList.value)
}
}
let getTitle = (menuList:any,path:string) =>{
for(let item of menuList){
if(item.route === path){
state.nowPageName = item.name
}
if(item.children){
getTitle(item.children,path)
}
}
}
onMounted(() => {
let cookieInfo = getCookie('userInfo') || ''
if(cookieInfo){
let menuListSession:any = sessionStorage.getItem('menuList')
menuList.value = JSON.parse(menuListSession)
userInfo.value = JSON.parse(cookieInfo)
state.selectedKeys = [route.path]
getBreadData(route.name)
getTitle(menuList.value, route.path)
}else{
router.replace('/login') //没登录
}
})
return{
...toRefs(state),
menuList,
showMenuList,
popoverModal,
userInfo,
handleClick,
logout,
routers,
}
},
});
</script>
<style lang="less">
.home_page{
width: 100%;
height: 100%;
display: flex;
background: rgba(243,244,248,0.4);
.home_menu_content{
padding-top: 20px;
.home_menu_log{
margin:0 0 27px 28px;
width: 144px;
height: 32px;
}
.ant-menu-dark.menu_list_content{
padding-left: 18px;
width: 300px;
height: calc(100% - 59px);
background: #FFFFFF;
flex-shrink: 0;
.menu_icon{
font-size: 24px;
color: #808185;
}
.ant-menu-item{
padding-left: 31px !important;
display: flex;
align-items: center;
height: 66px;
border-radius: 33px 0px 0px 33px;
box-sizing: border-box;
&.ant-menu-item-only-child{
padding-left: 56px !important;
}
}
.ant-menu-submenu-title{
padding-left: 31px !important;
height: 66px;
}
.ant-menu-inline.ant-menu-sub{
background: #FFFFFF;
}
.menu_title{
font-size: 18px;
font-family: Adobe Heiti Std;
font-weight: normal;
color: #808185;
}
.ant-menu-item-selected{
background: #F3F4F8;
.menu_title{
color: #343579;
}
.menu_icon{
color: #808185;
}
}
}
}
.home_right_content{
width: 100%;
height: 100%;
.home_content_header{
display: flex;
justify-content: space-between;
align-items: center;
height: 66px;
padding: 0 28px 0 30px;
.page_name{
border-left: solid 4px #161F29;
.page_name_title,.ant-breadcrumb-link{
font-size: 18px;
font-weight: normal;
color: #333333;
padding-left: 12px;
line-height: 18px;
}
}
.user_info{
display: flex;
align-items: center;
cursor: pointer;
.header_img{
width: 36px;
height: 36px;
border-right: 50%;
margin-right: 10px;
}
.user_name{
font-size: 16px;
color: #1A1A1A;
margin-right: 10px;
}
.header_icon{
font-size: 14px;
}
.turn_round_icon{
-moz-transform:rotate(180deg);
-webkit-transform:rotate(180deg);
transform: rotate(180deg);
animation-direction: 0.5s;
}
}
}
.home_content_body{
width: 100%;
height: calc(100% - 66px);
}
}
}
</style>
<style lang="less">
.select_item{
color: #4D4D4D;
display: flex;
align-items: center;
cursor: pointer;
user-select:none;
.iconfont{
font-size: 14px;
}
.select_item_des{
font-size: 13px;
margin-left: 8px;
}
}
</style>

357
src/views/LoginPage.vue Normal file
View File

@@ -0,0 +1,357 @@
<template>
<div class="login_page">
<div class="login_content">
<div class="login_content_body">
<img class="login_logo" src='@/assets/images/login_logo.png'>
<div class="login_form_content">
<img class="login_form_logo" src='@/assets/images/home_logo.png'>
<a-input class="login_form_input" size="large" placeholder="account" v-model:value="username">
<template #prefix>
<span class="icon iconfont icon-yonghuming form_icon_content"></span>
</template>
</a-input>
<a-input type="password" class="login_form_input" size="large" v-model:value="password" placeholder="password" >
<template #prefix>
<span class="icon iconfont icon-weibiaoti form_icon_content"></span>
</template>
</a-input>
<a-checkbox class="form_checkbox" v-model:checked="isRemember">Remenber Me</a-checkbox>
<a-button class="login_button" size="large" type="primary" @click="submitPerLogin">Login</a-button>
</div>
</div>
<div class="login_footer_content">copyright ©2023 CodeCreate</div>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent,onMounted,ref,reactive } from "vue";
import { Https } from "@/tool/https";
import { setCookie } from "@/tool/cookie";
import { message } from "ant-design-vue";
import { useRouter } from 'vue-router'
import { encode, decode } from 'js-base64';
export default defineComponent({
name:'login',
setup(){
const router = useRouter()
let username = ref('')
let password = ref('')
let isRemember = ref(false)
let menuList:any = reactive([{
name:'Workdesk',
code:'WORK_BENCH',
route:'/home/worktable',
icon:'icon-gongzuotai',
key:'/home/worktable',
isShow:false,
},
{
name:'Product Management',
code:'PRODUCT_LIST',
route:'/home/productmanage',
icon:'icon-shangpinguanli',
key:'/home/productmanage',
isShow:false,
},
{
name:'Store Management',
code:'STORE_LIST',
route:'/home/storemanage',
icon:'icon-mendianguanli',
key:'/home/storemanage',
isShow:false,
},
{
name:'Label Management',
code:'LABEL_LIST',
route:'/home/labelmanage',
icon:'icon-biaoqian',
key:'/home/labelmanage',
isShow:false,
},
{
name:'Setting',
code:'SYS_MANAGER',
route:'',
icon:'icon-xitongguanli',
key:'sub2',
expandIcon:'icon-xialajiantouxiao',
isShow:false,
children:[
{
code:'USER_MANAGER',
name:'User Management',
route:'/home/usermanage',
icon:'',
key:'/home/usermanage',
isShow:false,
},
{
code:'ROLE_MANAGER',
name:'Access Permission',
route:'/home/rolemanage',
icon:'',
key:'/home/rolemanage',
isShow:false,
},
]
},
{
name:'Export Excil',
code:'MI_TU_EXPORT',
route:'/home/exportExcil',
icon:'icon-xiazaiwenjian',
key:'/home/exportExcil',
isShow:false,
},
// {
// name:'Export Excil',
// code:'EXPORT_EXCIL',
// route:'/home/exportExcil',
// icon:'icon-xiazaiwenjian',
// key:'sub3',
// key:'/home/exportExcil',
// expandIcon:'icon-xialajiantouxiao',
// isShow:true,
// children:[
// {
// code:'USER_MANAGER',
// name:'User Management',
// route:'/home/excil1',
// icon:'',
// key:'/home/excil1',
// isShow:true,
// },
// {
// code:'ROLE_MANAGER',
// name:'Access Permission',
// route:'/home/excil2',
// icon:'',
// key:'/home/excil2',
// isShow:true,
// },
// ]
// }
])
let submitPerLogin = () =>{
if (!username) {
message.error("Please enter your account");
return;
}
if(!password){
message.error("Please enter your password");
return;
}
let data = {
userAccount: username.value,
password: encode(password.value),
};
Https.axiosPost(Https.httpUrls.accountLogin, data).then(
(rv: any) => {
if (rv) {
let token = rv.token;
setCookie("token", token);
setCookie("userInfo", JSON.stringify(rv));
rememberAccount()
getMenuRole()
}
}
);
}
let rememberAccount = () =>{
if(isRemember){
let accountInfo = {
account:username.value,
password:password.value,
}
localStorage.setItem('accountInfo',JSON.stringify(accountInfo))
}
}
let getMenuRole = () =>{
Https.axiosGet(Https.httpUrls.queryUsrPermission).then(
(rv: any) => {
if (rv) {
let rolePermission = rv.rolePermission.level1ResourceList
let setMenuListShow = (role:any) =>{
for(let menu of menuList){
if(role.code === menu.code){
console.log(menu,role);
menu.isShow = role.select == 1 ? true :false
if(role.operationList){
menu.operationList = role.operationList
}
break
}
if(menu.children){
for(let child of menu.children){
if(role.code == child.code){
child.isShow = role.select == 1 ? true :false
if(role.operationList){
child.operationList = role.operationList
}
break
}
}
}
}
}
for(let role of rolePermission){
setMenuListShow(role)
for(let level2 of role.level2ResourceList){
setMenuListShow(level2)
}
}
sessionStorage.setItem('menuList',JSON.stringify(menuList))
let worktable = menuList.filter((v:any) => v.code === 'WORK_BENCH')[0]
if(worktable.isShow){
router.push("/home/worktable")
}else{
message.error('You do not have permission to access this website, please contact your manager.')
}
}
}
);
}
onMounted(()=>{
if(localStorage.getItem('accountInfo')){
let accountInfo:any = localStorage.getItem('accountInfo')
accountInfo = JSON.parse(accountInfo)
username.value = accountInfo.account
password.value = accountInfo.password
isRemember.value = true
}
})
return {
username,
password,
isRemember,
submitPerLogin,
}
},
});
</script>
<style lang="less" scoped>
.login_page {
width: 100%;
height: 100%;
.login_content{
width: 1440px;
margin: 0 auto;
height: 100%;
position: relative;
display: flex;
align-items: center;
.login_content_body{
width: 100%;
padding: 0 160px 0 80px;
display: flex;
justify-content: space-between;
align-items: center;
.login_logo{
width: 457px;
height: 370px;
}
.login_form_content{
width: 600px;
height: 540px;
padding: 0 60px;
box-sizing: border-box;
background: #FFFFFF;
box-shadow: -3px 20px 59px 0px rgba(200,200,200,0.3);
border-radius: 10px;
.login_form_logo{
width: 165px;
margin: 78px auto 60px;
display: block;
}
.login_form_input{
width: 480px;
height: 50px;
border: 1px solid #DFDFDF;
border-radius: 25px;
margin-bottom: 29px;
}
.form_checkbox{
font-size: 16px;
color: #151515;
margin-bottom: 29px;
.ant-checkbox-inner{
width: 20px;
height: 20px;
border: 1px solid #DFDFDF;
border-radius: 4px;
}
}
.form_icon_content{
font-size: 18px;
color: #343579;
}
.login_button{
display: block;
width: 100%;
height: 50px;
background: #343579;
border-radius: 25px;
font-size: 18px;
font-family: Roboto;
font-weight: 500;
color: #FFFFFF;
}
}
}
.login_footer_content{
width: 100%;
font-size: 13px;
font-family: Roboto;
font-weight: 400;
color: #151515;
text-align: center;
position: absolute;
bottom: 68px;
}
}
}
</style>
<style lang="less">
.login_page {
.ant-input-prefix{
margin: 0px 15px;
}
.ant-input-affix-wrapper:focus{
border-color: #343579 !important;
box-shadow: none;
}
.ant-input-affix-wrapper-focused{
border-color: #343579 !important;
box-shadow: none;
}
}
</style>

View File

@@ -0,0 +1,55 @@
<template>
<div class="no_permission_page">
<div class="no_permission_page_content">
<img class="no_permission_page_img" src="@/assets/images/no_permission.png">
<div class="no_permission_tip">
<span>对不起您暂时无访问权限</span>
<router-link to="/home/worktable">点击返工作台</router-link>
</div>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent} from "vue";
export default defineComponent({
name:'noPermission',
setup(){
return {
}
},
});
</script>
<style lang="less" scoped>
.no_permission_page{
width: 100%;
height: 100%;
background: #F6F6F6;
display: flex;
justify-content: center;
align-items: center;
.no_permission_page_content{
text-align: center;
margin-top: -100px;
.no_permission_page_img{
width: 593px;
margin-bottom: 60px;
}
.no_permission_tip{
font-size: 16px;
font-family: PingFang SC;
font-weight: 500;
color: #151515;
}
}
}
</style>

View File

@@ -0,0 +1,241 @@
<template>
<div class="export_manage_page list_page">
<div class="list_page_content">
<div class="list_top_content">
<div class="list_top_search_content">
<div class="search_content_left">
<filterComponent :title="'Role Name or Id'"><a-input v-model:value="name" size="large" placeholder="Please input Role Name or Id" @keydown.enter="searchList()"/></filterComponent>
<filterComponent :title="'Period'">
<a-select
v-model:value="type"
size="large"
style="width:280px"
:options="labelTypeList"
allowClear
></a-select>
</filterComponent>
<filterComponent :title="'Export Time'"> <a-range-picker size="large" v-model:value="date" :placeholder="['Start Time', 'End Time']" format="YYYY-MM-DD" valueFormat="YYYY-MM-DD"/></filterComponent>
</div>
</div>
<div class="list_top_button_content">
<a-button class="primary_button btn-margin-r-20" type="primary" size="large" @click="searchList()">Seach</a-button>
<a-button class="default_button" size="large" @click="resetList()">Reset</a-button>
</div>
</div>
<div class="list_table_content">
<a-table :columns="columns" :data-source="collectionList" @change="changePage" :loading="tableLoading"
:pagination="{
showSizeChanger:true,
current: currentPage,
pageSize:pageSize,
total: total,
showQuickJumper:true,
bordered:false,
pageSizeOptions:['10','20','50'],
}">
<template v-slot:bodyCell="{column,record, index}" >
<template v-if="column.dataIndex === 'operation'">
<div class="operate_list">
<div class="operate_item" @click="setExport(record,index)">Export</div>
</div>
</template>
</template>
</a-table>
</div>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent,ref,reactive,toRefs,UnwrapRef,onMounted,createVNode } from "vue";
import filterComponent from '@/component/filterComponent.vue'
import { message,Modal } from "ant-design-vue";
import { Moment } from 'moment';
import { Https } from "@/tool/https";
import { WarningOutlined } from '@ant-design/icons-vue';
export default defineComponent({
name:'roleManage',
components:{filterComponent},
setup(){
let filter = reactive({
name:'',
type:'',
date:ref<Moment[]>([])
})
let tableLoading = ref(false)
let columns = reactive([
{ title: 'Name', align:'center', ellipsis: true, dataIndex: 'exportName', key: 'exportName', width:250 },
{ title: 'Create Time', align:'center', ellipsis: true, dataIndex: 'createTime', key: 'createTime'},
{ title: 'Type', align:'center', ellipsis: true, dataIndex: 'span', key: 'span'},
{
title: 'Operations',
key: 'operation',
align:'center',
fixed: 'right',
// width: 250,
dataIndex:'operation',
},
])
let collectionList = ref([])
let labelTypeList:any = ref([
{ value:'week',label:'Week'},
{ value:'month',label:'Month'},
{ value:'year',label:'Year'},
])
let currentPage = ref(1)
let pageSize = ref(10)
let total = ref(1)
let changePage = (e:any) =>{
currentPage.value = e.current
pageSize.value = e.pageSize
getExportList()
}
let getExportList = () =>{
let data = {
page:currentPage.value,
size:pageSize.value,
fileName:filter.name,
span:filter.type,
endTime:filter.date[1]?filter.date[1]:'',
startTime:filter.date[0]?filter.date[0]:'',
}
tableLoading.value = true
Https.axiosPost(Https.httpUrls.miTuExportPage, data).then(
(rv: any) => {
if (rv) {
tableLoading.value = false
if(currentPage.value > 1 && rv.content.length == 0){
currentPage.value = 1
getExportList()
}else{
collectionList.value = rv.content
total = rv.total
}
}
}
).catch((res:any)=>{
tableLoading.value = false
})
}
let searchList = () =>{
currentPage.value = 1
getExportList()
}
let resetList = () =>{
currentPage.value = 1
filter.name = ''
getExportList()
}
let setExport = (data:any,index:any) =>{
Https.axiosGet(Https.httpUrls.miTuExportExport+`/${data.id}`,{responseType: 'blob'}).then((rv)=>{
let name = rv.name.split('=')[1];
let url = window.URL.createObjectURL(new Blob([rv.data], { type: rv.type }));
const link = document.createElement('a');
link.download = name; //定义表格名称,后缀是文件格式
link.style.display = 'none';
link.href = url;
document.body.appendChild(link);
link.click();
URL.revokeObjectURL(link.href)
document.body.removeChild(link);
})
}
onMounted(() => {
getExportList()
})
return {
...toRefs(filter),
tableLoading,
columns,
labelTypeList,
collectionList,
currentPage,
pageSize,
total,
changePage,
searchList,
resetList,
setExport,
}
},
});
</script>
<style lang="less" scoped>
.export_manage_page{
padding-left: 28px;
.export_pic{
width: 50px;
}
.operate_list{
display: flex;
// justify-content: space-between;
justify-content: center;
padding: 0 50px;
.operate_item{
font-size: 14px;
font-family: Roboto;
font-weight: 400;
color: #343579;
cursor: pointer;
}
}
}
.form_check_all{
margin: 10px 0;
}
.permission_table{
width: 100%;
margin-top: 10px;
border-left:1px solid #DFDFDF;
border-top:1px solid #DFDFDF;
td{
border-bottom: 1px solid #DFDFDF;
border-right: 1px solid #DFDFDF;
.table_td_block{
padding: 16px 0 16px 20px;
border-bottom: 1px solid #DFDFDF;
&:last-child{
border-bottom: none;
}
}
.table_td_permission{
display: flex;
min-height: 23px;
}
}
.first_td{
width: 120px;
}
.second_td{
width: 150px;
}
}
</style>

View File

@@ -0,0 +1,533 @@
<template>
<div class="user_manage_page list_page">
<div class="list_page_content">
<div class="list_top_content">
<div class="list_top_search_content">
<div class="search_content_left">
<filterComponent :title="'User Name'"><a-input v-model:value="userName" size="large" placeholder="Please input User Name" @keydown.enter="searchList()"/></filterComponent>
<filterComponent :title="'User Phone'"><a-input v-model:value="userPhone" size="large" placeholder="Please input User Phone" @keydown.enter="searchList()" /></filterComponent>
<filterComponent :title="'User Role'">
<a-select v-model:value="roleId" size="large" style="width:280px" placeholder="Please select" :options="userRoleList" allowClear></a-select>
</filterComponent>
<filterComponent :title="'User State'">
<a-select v-model:value="state" size="large" style="width:280px" placeholder="Please select" :options="userStateList" allowClear></a-select>
</filterComponent>
<filterComponent :title="'Add Time'"><a-range-picker v-model:value="addTime" size="large" :placeholder="['Start Time', 'End Time']" format="YYYY-MM-DD" valueFormat="YYYY-MM-DD"/></filterComponent>
</div>
</div>
<div class="list_top_button_content">
<a-button class="primary_button btn-margin-r-20" type="primary" size="large" @click="searchList()">Seach</a-button>
<a-button class="default_button" size="large" @click="resetList()">Reset</a-button>
<a-button class="primary_button btn-margin-t-35" type="primary" size="large" @click="addUser()">+Add User</a-button>
</div>
</div>
<div class="list_table_content">
<a-table :columns="columns" :data-source="collectionList" @change="changePage" :loading="tableLoading"
:pagination="{
showSizeChanger:true,
current: currentPage,
pageSize:pageSize,
total: total,
showQuickJumper:true,
bordered:false,
pageSizeOptions:['10','20','50'],
}">
<template v-slot:bodyCell="{column,record, index}" >
<template v-if="column.dataIndex === 'stateName'">
<div :class="['state_name_block', record.state == 1 ? 'enable_state' :'disable_state']">
<div class="state_round"></div>
<div>{{record.stateName}}</div>
</div>
</template>
<template v-if="column.dataIndex === 'operation'">
<div class="operate_list">
<div class="operate_item" @click="editUser(record)">Edit</div>
<div :class="['operate_item', record.state == 0 ? '' :'operate_disable_state']" @click="enableUser(record,index)">
<div v-if="record.state == 0">Enable</div>
<div v-else>Disable</div>
</div>
<div class="operate_item" @click="deleteUser(record,index)">Delete</div>
</div>
</template>
</template>
</a-table>
</div>
</div>
<a-modal class="edit_modal_component"
:destroyOnClose="true"
v-model:visible="editUserModal"
:footer="null"
:title="editUserTitle"
width="680px"
:maskClosable="false"
:centered="true"
@cancel="closeUser"
>
<a-form ref="formRef" :model="formState" :rules="rules" :layout="'vertical'" >
<a-row :gutter="[16,16]">
<a-col :span="12">
<a-form-item label="User Name" name="userName">
<a-input v-model:value="formState.userName" size="large" placeholder="Please input user name"/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="User Account" name="userAccount">
<a-input v-model:value="formState.userAccount" size="large" placeholder="Please input user account"/>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="[16,16]">
<a-col :span="12">
<a-form-item label="User Code" name="userPassword">
<a-input v-model:value="formState.userPassword" size="large" placeholder="Please input user code" type="password"/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="User Phone" name="userPhone">
<a-input v-model:value="formState.userPhone" size="large" placeholder="Please input user phone"/>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="[16,16]">
<a-col :span="12">
<a-form-item label="App User" name="appUser">
<a-select v-model:value="formState.appUser" size="large" :options="appUserList" placeholder="Please select"></a-select>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="User Role" name="roleId">
<a-select v-model:value="formState.roleId" size="large" :options="userRoleList" placeholder="Please select"></a-select>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="[16,16]">
<a-col :span="24">
<a-form-item label="Owner Store" name="storeIds">
<a-select v-model:value="formState.storeIds" size="large" :options="ownerStoreList" mode="multiple" placeholder="Please select"></a-select>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="[16,16]">
<a-col :span="24">
<a-form-item label ="Remarks" name="remarks">
<a-input v-model:value="formState.remarks" size="large" placeholder="Please input remarks"/>
</a-form-item>
</a-col>
</a-row>
</a-form>
<div class="modal_button_list">
<a-button class="default_button btn-margin-r-20" size="large" @click="closeUser">Cancel</a-button>
<a-button class="primary_button" type="primary" size="large" @click="confirmSubmit">Submit</a-button>
</div>
</a-modal>
</div>
</template>
<script lang="ts">
import { defineComponent,ref,reactive,toRefs,UnwrapRef,onMounted,createVNode } from "vue";
import filterComponent from '@/component/filterComponent.vue'
import { Moment } from 'moment';
import { message,Modal } from "ant-design-vue";
import { Https } from "@/tool/https";
import { WarningOutlined } from '@ant-design/icons-vue';
import { encode, decode } from 'js-base64';
import { useRoute } from 'vue-router'
import { formatTime, startTime, endTime, exportExcil } from "@/tool/util"
export default defineComponent({
name:'userManage',
components:{filterComponent},
setup(){
const route = useRoute()
let filter:any = reactive({
userName:'',
userPhone:'',
roleId:[],
state:[],
addTime:ref<Moment[]>([]),
})
let tableLoading = ref(false)
let userRoleList:any = ref([])
let userStateList:any = ref([
{value:'1',label:'Activated'},
{value:'0',label:'Deactivated'},
])
let appUserList:any = ref([
{value:1,label:'Yes'},
{value:0,label:'No'},
])
let ownerStoreList:any = ref([])
let columns = reactive([
{ title: 'User Name', align:'center', ellipsis: true, dataIndex: 'userName', key: 'userName' },
{ title: 'User Account', align:'center', ellipsis: true, dataIndex: 'userAccount', key: 'userAccount'},
{ title: 'Add Role', align:'center', ellipsis: true, dataIndex: 'roleName', key: 'roleName' },
{ title: 'User Phone', align:'center', ellipsis: true, dataIndex: 'userPhone', key: 'userPhone' },
{ title: 'Owned Store', align:'center', ellipsis: true, dataIndex: 'storeName', key: 'storeName',customRender:(record:any)=>{
let storeNameNew = record.text.join(',')
return storeNameNew
}
},
{ title: 'Add Time', align:'center', ellipsis: true, dataIndex: 'createDate', key: 'createDate' ,customRender:(record:any)=>{
let time = formatTime(record.text / 1000, 'YYYY-MM-DD hh:mm:ss')
return time
},
},
{ title: 'Activated', align:'center', ellipsis: true, dataIndex: 'appUserName', key: 'appUserName' },
{ title: 'User State', align:'center', ellipsis: true, dataIndex: 'stateName', key: 'stateName' },
{
title: 'Operations',
key: 'operation',
align:'center',
fixed: 'right',
width: 150,
dataIndex:'operation',
},
])
let collectionList:any = ref([])
let currentPage = ref(1)
let pageSize = ref(10)
let total = ref(1)
let editUserModal = ref(false)
let editUserTitle = ref('Add user')
let formRef = ref();
let formState:any = ref({
id: '',
userName: '',
userAccount: '',
userPassword:'',
userPhone:'',
appUser:null,
roleId:null,
storeIds:[],
remarks:'',
});
let rules = {
userName: [
{ required: true, message: 'Please input user name', trigger: 'blur' },
],
userAccount: [
{ required: true, message: 'Please input user account', trigger: 'blur' },
{ min: 6, max: 20, message: 'Length should be 6 to 20', trigger: 'blur' },
],
userPassword: [
{ required: true, message: 'Please input user code', trigger: 'blur' },
{ min: 6, max: 20, message: 'Length should be 6 to 20', trigger: 'blur' },
],
userPhone: [
{ required: true, message: 'Please input user phone', trigger: 'blur' },
{ pattern: /^\d*$/, message: 'The format of the mobile phone number is incorrect', trigger: 'blur' },
],
appUser: [
{ required: true, message: 'Please select app user', trigger: 'blur' },
],
userRole: [
{ required: true, message: 'Please select user role', trigger: 'blur' },
],
ownerStore: [
{ required: true, message: 'Please select owner store', trigger: 'blur' },
],
}
let changePage = (e:any) =>{
currentPage.value = e.current
pageSize.value = e.pageSize
getUserlist()
}
let closeUser = () =>{
formState.value = {
id: '',
userName: '',
userAccount: '',
userPassword:'',
userPhone:'',
appUser:null,
roleId:null,
storeIds:[],
remarks:'',
}
editUserModal.value = false
}
let addUser = () =>{
editUserTitle.value = 'Add user'
editUserModal.value = true
// const data = [
// [
// ['12312312312'],
// ['姓名', '年龄', '性别'],
// ['张三', 20, '男'],
// ['李四', 25, '女']
// ],
// [
// ['城市', '人口'],
// ['北京', 21540000],
// ['上海', 24240000]
// ]
// ];
// const sheetNames = ['Sheet1', 'Sheet2'];
// const fileName = 'output.xlsx';
// const configuration = {
// width:15,
// titleCell:2,
// }
// exportExcil(data,sheetNames,fileName,configuration)
}
let editUser = (data:any,) =>{
editUserModal.value = true
editUserTitle.value = 'Eidt user'
formState.value = {
id: data.id,
userName: data.userName,
userAccount:data.userAccount,
userPassword:data.userPassword ? decode(data.userPassword) : '',
userPhone:data.userPhone,
appUser:data.appUser,
roleId:String(data.roleId),
storeIds:data.storeIds,
remarks:data.remarks,
}
}
let getUserlist = () =>{
let data = {
userName : filter.userName,
userPhone:filter.userPhone,
state:filter.state && filter.state.length ? filter.state : '',
roleId:filter.roleId && filter.roleId.length ? filter.roleId : '',
createDateStart:filter.addTime && filter.addTime.length ? startTime(filter.addTime[0]) :'',
createDateEnd:filter.addTime && filter.addTime.length ? endTime(filter.addTime[1]) :'',
page:currentPage.value,
size:pageSize.value
}
tableLoading.value = true
Https.axiosPost(Https.httpUrls.accountQueryUserPage, data).then(
(rv: any) => {
if (rv) {
tableLoading.value = false
collectionList.value = rv.content
total.value = rv.total
}
}
);
}
let getRolelist = () =>{
Https.axiosPost(Https.httpUrls.roleQueryAll, {}).then(
(rv: any) => {
if (rv) {
userRoleList.value = rv.map((v:any)=>{
let data = {
...v,
value:v.id,
label:v.name
}
return data
})
}
}
);
}
let searchList = () =>{
currentPage.value = 1
getUserlist()
}
let resetList = () =>{
currentPage.value = 1
filter.userName = ''
filter.userPhone = ''
filter.roleId = []
filter.state = []
filter.addTime = ref<Moment[]>([])
getUserlist()
}
let confirmSubmit = () =>{
let data = {
...formState.value,
timeZone:Intl.DateTimeFormat().resolvedOptions().timeZone,
}
data.userPassword = encode(data.userPassword)
let submit = () =>{
Https.axiosPost(Https.httpUrls.accountSaveOrEdit, data).then(
(rv: any) => {
if (rv) {
let tip = !formState.value.id ? 'User added successfully' :'Edit user successfully'
message.success(tip)
resetList()
closeUser()
}
}
);
}
formRef.value.validate().then(()=>{
submit()
})
}
let enableUser = (record:any,index:any) =>{
let data ={
id:record.id,
enable:record.state == 1 ? 0 : 1
}
Https.axiosPost(Https.httpUrls.accountEnable, data).then(
(rv: any) => {
if (rv) {
let tip = record.state == 0? 'Enable successfully' :'Disable successfully'
message.success(tip)
collectionList.value[index].state = data.enable
collectionList.value[index].stateName = data.enable == 1 ? 'Activated' :'Deactivated'
}
}
);
}
let deleteUser = (data:any,index:any) =>{
let confirmDelete = (data:any,index:any) =>{
let newData = {
id:data.id
}
Https.axiosPost(Https.httpUrls.accountDelete, newData).then(
(rv: any) => {
if (rv) {
message.success('Delete success')
collectionList.value.splice(index,1)
}
}
);
}
Modal.confirm({
title: "Are you sure delete the user?",
icon: createVNode(WarningOutlined),
class:'confirm_style',
okText: 'Ok',
cancelText: 'Cancel',
// centered:true,
onOk() {
confirmDelete(data,index)
}
});
}
let storeQueryAll = () =>{
Https.axiosPost(Https.httpUrls.storeQueryAll,{}).then(
(rv: any) => {
if (rv) {
ownerStoreList.value = rv.map((v:any)=>{
let data = {
...v,
value:v.id,
label:v.name
}
return data
})
}
}
);
}
onMounted(() => {
if(route.query && route.query.type === 'add'){
addUser()
}
getUserlist()
storeQueryAll()
getRolelist()
})
return {
...toRefs(filter),
tableLoading,
userRoleList,
userStateList,
appUserList,
ownerStoreList,
columns,
collectionList,
currentPage,
pageSize,
total,
editUserModal,
editUserTitle,
formRef,
formState,
rules,
changePage,
closeUser,
addUser,
editUser,
searchList,
resetList,
confirmSubmit,
enableUser,
deleteUser
}
},
});
</script>
<style lang="less" scoped>
.user_manage_page{
padding-left: 28px;
.user_pic{
width: 50px;
}
.state_name_block{
display: flex;
align-items: center;
font-size: 14px;
justify-content: center;
&.enable_state{
color: #47E417;
.state_round{
background: #47E417;
}
}
&.disable_state{
color: #E41335;
.state_round{
background: #E41335;
}
}
.state_round{
width: 14px;
height: 14px;
border-radius: 50%;
margin-right: 5px;
}
}
.operate_list{
display: flex;
justify-content: space-between;
.operate_item{
font-size: 14px;
font-family: Roboto;
font-weight: 400;
color: #343579;
cursor: pointer;
&.operate_disable_state{
color: #E41335;
}
}
}
}
</style>

View File

@@ -0,0 +1,321 @@
<template>
<div class="label_manage_page list_page">
<div class="list_page_content">
<div class="list_top_content">
<div class="list_top_search_content">
<div class="search_content_left">
<filterComponent :title="'Label Name'"><a-input v-model:value="name" size="large" placeholder="Please input Label Name or Id" @keydown.enter="searchList"/></filterComponent>
<filterComponent :title="'Label attributes'">
<a-select
v-model:value="type"
size="large"
style="width:280px"
:options="labelTypeList"
placeholder="Please select"
allowClear
></a-select>
</filterComponent>
<filterComponent :title="'Add Time'"><a-range-picker v-model:value="addTime" size="large" :placeholder="['Start Time', 'End Time']" format="YYYY-MM-DD" valueFormat="YYYY-MM-DD"/></filterComponent>
</div>
</div>
<div class="list_top_button_content">
<a-button class="primary_button btn-margin-r-20" type="primary" size="large" @click="searchList()">Seach</a-button>
<a-button class="default_button" size="large" @click="resetList()">Reset</a-button>
<a-button class="primary_button btn-margin-t-35" type="primary" size="large" @click="addLabel()">+Add Label</a-button>
</div>
</div>
<div class="list_table_content">
<a-table :columns="columns" :data-source="collectionList" @change="changePage" :loading="tableLoading"
:pagination="{
showSizeChanger:true,
current: currentPage,
pageSize:pageSize,
total: total,
showQuickJumper:true,
bordered:false,
pageSizeOptions:['10','20','50'],
}">
<template v-slot:bodyCell="{column,record, index}" >
<template v-if="column.dataIndex === 'operation'">
<div class="operate_list">
<div class="operate_item" @click="editLabel(record)">Edit</div>
<div class="operate_item" @click="deleteLabel(record,index)">Delete</div>
</div>
</template>
</template>
</a-table>
</div>
</div>
<a-modal class="edit_modal_component"
:destroyOnClose="true"
v-model:visible="editLabelModal"
:footer="null"
:title="editLabelTitle"
width="560px"
:maskClosable="false"
:centered="true"
@cancel="closeLabel"
>
<a-form ref="formRef" :model="formState" :rules="rules" :layout="'vertical'" >
<a-form-item label="Label Name" name="name">
<a-input v-model:value="formState.name" size="large" placeholder="Please input label name" />
</a-form-item>
<a-form-item label="Label attributes" name="type">
<a-select v-model:value="formState.type" size="large" :options="labelTypeList" placeholder="Please select label attributes"></a-select>
</a-form-item>
<a-form-item label="Remarks" name="remarks">
<a-input v-model:value="formState.remarks" size="large" placeholder="Please input remarks"/>
</a-form-item>
</a-form>
<div class="modal_button_list">
<a-button class="default_button btn-margin-r-20" size="large" @click="closeLabel">Cancel</a-button>
<a-button class="primary_button" type="primary" size="large" @click="confirmSubmit">Submit</a-button>
</div>
</a-modal>
</div>
</template>
<script lang="ts">
import { defineComponent,ref,reactive,toRefs,UnwrapRef,onMounted,createVNode } from "vue";
import filterComponent from '@/component/filterComponent.vue'
import { Moment } from 'moment';
import { message,Modal } from "ant-design-vue";
import { WarningOutlined } from '@ant-design/icons-vue';
import { Https } from "@/tool/https";
import { formatTime, startTime, endTime } from "@/tool/util"
export default defineComponent({
name:'labelManage',
components:{filterComponent},
setup(){
let filter:any = reactive({
name:'',
type:'',
addTime:ref<Moment[]>([])
})
let tableLoading = ref(false)
let labelTypeList:any = ref([
{ value:'CUSTOM',label:'Custom tags'},
{ value:'NEW_PRODUCT',label:'New in'},
{ value:'SALE',label:'Discount'},
])
let labelTypeData:any = {
'CUSTOM':'Custom tags',
'NEW_PRODUCT':'New in',
'SALE':'Discount'
}
let columns = reactive([
{ title: 'Label Name', align:'center', ellipsis: true, dataIndex: 'name', key: 'name' },
{ title: 'Label attributes', align:'center', ellipsis: true, dataIndex: 'type', key: 'type', customRender:(record:any)=>{
let labelType = labelTypeData[record.text]
return labelType },
},
{ title: 'Add User', align:'center', ellipsis: true, dataIndex: 'createUserName', key: 'createUserName' },
{ title: 'Label Product Counts', align:'center', ellipsis: true, dataIndex: 'counts', key: 'counts' },
{ title: 'Remarks', align:'center', ellipsis: true, dataIndex: 'remarks', key: 'remarks' },
{ title: 'Add Time', align:'center', ellipsis: true, dataIndex: 'createDate', key: 'createDate',customRender:(record:any)=>{
let time = formatTime(record.text / 1000, 'YYYY-MM-DD hh:mm:ss')
return time },
},
{
title: 'Operations',
key: 'operation',
align:'center',
fixed: 'right',
width: 150,
dataIndex:'operation',
},
])
let collectionList = ref([])
let currentPage = ref(1)
let pageSize = ref(10)
let total = ref(1)
let editLabelModal = ref(false)
let editLabelTitle = ref('Add Label')
let formRef = ref();
let formState = ref({
id: '',
name: '',
type:[],
remarks:'',
});
let rules = reactive({
name: [
{ required: true, message: 'Please input label name', trigger: 'blur' },
],
type: [
{ required: true, message: 'Please select label attributes', trigger: 'blur' },
],
})
let changePage = (e:any) =>{
currentPage.value = e.current
pageSize.value = e.pageSize
getLabelist()
}
let closeLabel = () =>{
formState.value = {
id: '',
name: '',
type:[],
remarks:''
}
editLabelModal.value = false
}
let addLabel = () =>{
editLabelTitle.value = 'Add Label'
editLabelModal.value = true
}
let editLabel = (data:any,) =>{
editLabelModal.value = true
editLabelTitle.value = 'Eidt Label'
formState.value = {
id: data.id,
name: data.name,
type:data.type,
remarks:data.remarks,
}
}
let getLabelist = () =>{
let data = {
name : filter.name,
type:filter.type,
createDateStart:filter.addTime && filter.addTime.length ? startTime(filter.addTime[0]) : '',
createDateEnd:filter.addTime && filter.addTime.length ? endTime(filter.addTime[1]) : '',
page:currentPage.value,
size:pageSize.value
}
tableLoading.value = true
Https.axiosPost(Https.httpUrls.labelQueryStorePage, data).then(
(rv: any) => {
if (rv) {
tableLoading.value = false
collectionList.value = rv.content
total.value = rv.total
}
}
);
}
let confirmSubmit = () =>{
let data = {
...formState.value,
timeZone:Intl.DateTimeFormat().resolvedOptions().timeZone,
}
formRef.value.validate().then(()=>{
submit()
})
let submit = () => {
Https.axiosPost(Https.httpUrls.labelSaveOrEdit, data).then(
(rv: any) => {
if (rv) {
let tip = !formState.value.id ? 'Label added successfully' :'Edit label successfully'
message.success(tip)
resetList()
closeLabel()
}
}
);
}
}
let searchList = () =>{
currentPage.value = 1
getLabelist()
}
let resetList = () =>{
currentPage.value = 1
filter.name = ''
filter.type = ''
filter.addTime = ref<Moment[]>([])
getLabelist()
}
let deleteLabel = (data:any,index:any) =>{
let confirmDelete = (data:any,index:any) =>{
let newData = {
id:data.id
}
Https.axiosPost(Https.httpUrls.labelDelete, newData).then(
(rv: any) => {
if (rv) {
message.success('Delete success')
collectionList.value.splice(index,1)
}
}
);
}
Modal.confirm({
title: "Are you sure about the label? The label associated with the product will become invalid after deletion!",
icon: createVNode(WarningOutlined),
class:'confirm_style',
okText: 'Ok',
cancelText: 'Cancel',
// centered:true,
onOk() {
confirmDelete(data,index)
}
});
}
onMounted(() => {
getLabelist()
})
return {
...toRefs(filter),
tableLoading,
labelTypeList,
columns,
collectionList,
currentPage,
pageSize,
total,
editLabelModal,
editLabelTitle,
formRef,
formState,
rules,
changePage,
closeLabel,
addLabel,
editLabel,
confirmSubmit,
searchList,
resetList,
deleteLabel
}
},
});
</script>
<style lang="less" scoped>
.label_manage_page{
padding-left: 28px;
.operate_list{
display: flex;
justify-content: space-between;
.operate_item{
font-size: 14px;
font-family: Roboto;
font-weight: 400;
color: #343579;
cursor: pointer;
}
}
}
</style>

View File

@@ -0,0 +1,485 @@
<template>
<div class="productDetail_page">
<div class="productDetail_page_cotent">
<div class="productDetail_page_cotent_center">
<div class="detail_item_block">
<div class="detail_item_title_block">
<span class="icon iconfont icon-a-gengduocaidangongneng detail_icon"></span>
<div>Product image</div>
</div>
<div class="product_master_diagram">
<img class="product_master_img" :src="formState.pictureUrl">
</div>
</div>
<div class="detail_item_block">
<div class="detail_item_title_block">
<span class="icon iconfont icon-a-gengduocaidangongneng detail_icon"></span>
<div>Product label(s)</div>
</div>
<div class="detail_item_content">
<div class="productCategory_block">
<div class="produce_label_item" :style="{background:colorMap[label.type]}" v-for="label in productLabelList" :key="label.id">{{label.name}}</div>
</div>
</div>
</div>
<div class="detail_item_block">
<div class="detail_item_title_block">
<span class="icon iconfont icon-a-gengduocaidangongneng detail_icon"></span>
<div>Pirce</div>
</div>
<div class="detail_item_content">
<div class="productCategory_block">${{formState.price}}</div>
</div>
</div>
<div class="detail_item_block">
<div class="detail_item_title_block">
<span class="icon iconfont icon-a-gengduocaidangongneng detail_icon"></span>
<div>Inventory</div>
</div>
<div class="detail_item_content">
<a-table class="form_width_100" :columns="columns" :data-source="formState.storeList" bordered
:pagination="false" :scroll="{y: 400 }">
</a-table>
</div>
</div>
<div class="detail_item_block">
<div class="detail_item_title_block">
<span class="icon iconfont icon-a-gengduocaidangongneng detail_icon"></span>
<div>Label attributes</div>
</div>
<div class="detail_item_content">
<div class="productCategory_block">{{formState.productCategory}}</div>
</div>
<div>
<div class="detail_item_sec_title sec_title_margin">Attribute(s)</div>
<div class="detail_item_content">
<div class="attribute_block" v-if="formState.productAttribute?.length">
<div class="attribute_block_item" v-for="attr in formState.productAttribute" :key="attr.value">
<div class="attribute_block_item_content">
<div class="attribute_item_title">{{attr.labelType}}</div>
<div class="attribute_item_value" :title="attr.attributeValueList.join(',')">{{attr.attributeValueList.join(',')}}</div>
</div>
</div>
</div>
<div v-else class="null_data_block">
<img class="null_data_img" src="@/assets/images/null_img.png">
</div>
</div>
</div>
</div>
<div class="detail_item_block">
<div class="detail_item_title_block">
<span class="icon iconfont icon-a-gengduocaidangongneng detail_icon"></span>
<div>Mix and Match(es)</div>
</div>
<div class="detail_item_content">
<div class="product_match_block">
<div class="product_match_item" v-for="(match,index) in formState.assortmentList" :key="match.title" v-show="index<5 || assortmentShowMore">
<div class="product_match_item_header">
<div>Look{{index+1}}</div>
</div>
<div class="product_match_item_content">
<div class="match_item_img_content" v-for="img in match" :key="img">
<div class="match_item_img_block" @click="showMatchDetail(img.productId)">
<img :src="img.generatePictureUrl">
</div>
<div class="product_price">${{img.price}}</div>
</div>
</div>
</div>
</div>
<a-button class="primary_button show_more_button" type="primary" @click="assortmentListShowMore()" v-show="formState.assortmentList.length>5">{{assortmentShowMore?'Hide':'See More'}}</a-button>
</div>
</div>
</div>
</div>
<ProductDetailModal ref="productDetailModalDom"></ProductDetailModal>
</div>
</template>
<script lang="ts">
import { defineComponent,ref,onMounted,reactive } from "vue";
import { message } from "ant-design-vue";
import { useRouter,useRoute } from 'vue-router'
import { Https } from "@/tool/https";
import ProductDetailModal from '@/component/productComponent/productDetailModal.vue'
export default defineComponent({
name:'productDetail',
components:{ProductDetailModal},
setup(){
const route = useRoute()
let productDetail:any = ref([])
let productDetailModalDom:any = ref()
let formState:any = ref({
productPic:'',
productLabel:[],
inventoryData:[],
storeList:[],
storeNameList:[],
productCategory:'',
productAttribute:[],
assortmentList:[]
});
let assortmentShowMore = ref(false)
let productCategoryList:any = ref([])
let productAttributeList = ref([])
let columns = reactive([
{ title: 'Store', align:'center', ellipsis: true, dataIndex: 'storeName', key: 'storeName',width:"200px" },
{
title: 'Inventory',
align:'center',
ellipsis: true,
dataIndex: 'inventory',
key: 'inventory',
children: [
{title: 'XS',dataIndex: 'XS',key: 'XS', align:'center', },
{title: 'S',dataIndex: 'S',key: 'S', align:'center',},
{title: 'M',dataIndex: 'M',key: 'M', align:'center',},
{title: 'L',dataIndex: 'L',key: 'L', align:'center',},
{title: 'XL',dataIndex: 'XL',key: 'XL', align:'center',},
{title: 'XXL',dataIndex: 'XXL',key: 'XXL', align:'center',},
],
},
])
let colorMap = ref({
CUSTOM:'#798BD9',
NEW_PRODUCT:'#FFC384',
SALE:'#79D97C',
})
let productLabelList:any = ref([])
let getProductLabelList = () =>{
Https.axiosPost(Https.httpUrls.queryProductLabel).then(
(rv: any) => {
if (rv) {
productLabelList.value = rv.filter((v:any) => formState.value.productLabel.indexOf(v.id) > -1)
}
}
);
}
let getStoreInfo = (storeInfo:any) =>{
let storeList = []
let storeNameList:any = []
for(let item of storeInfo){
let sizeData:any = {}
for(let size of item.stock){
sizeData[size.size] = size.num
}
let data = {
storeName:item.storeName,
storeId:item.storeId,
...sizeData,
}
storeList.push(data)
}
storeInfo.forEach((item:any) => {
item.stock.forEach((stockItem:any) => {
storeNameList.push(stockItem.size);
});
});
// 排序并去重
storeNameList = storeNameList.sort().filter((value:any, index:any, array:any) => {
return array.indexOf(value) === index;
});
let children:any = []
storeNameList.forEach((item:any) => {
children.push({
title:item,
dataIndex:item,
key:item,
align:'center'
})
});
columns[1].children = children
return {storeList,storeNameList}
}
let getProductDetail = (id:any) =>{
return new Promise((resolve:any,rj:any)=>{
Https.axiosGet(Https.httpUrls.productDetail+'?id='+id).then(
(rv: any) => {
if (rv) {
let store = getStoreInfo(rv.storeInfoList)
formState.value = {
pictureUrl:rv.pictureUrl,
productLabel:rv.productLabelInfo.map((v:any)=>v.id),
price:rv.price,
productCategory:rv.attributeItemInfo.labelItem,
productAttribute:rv.attributeItemInfo.attributeTypeList,
storeList:store.storeList,
storeNameList:store.storeNameList,
assortmentList:rv.assortmentList,
}
productDetail.value = rv
resolve(rv)
}
}
);
})
}
let showMatchDetail = (productId:any) =>{
productDetailModalDom.value.openProductDetailModal(productId)
}
let assortmentListShowMore = () =>{
assortmentShowMore.value = !assortmentShowMore.value
}
onMounted(() => {
getProductDetail(route.query.id).then(rv=>{
getProductLabelList()
})
})
return {
productDetailModalDom,
columns,
formState,
assortmentShowMore,
productCategoryList,
productAttributeList,
colorMap,
productLabelList,
showMatchDetail,
assortmentListShowMore,
}
},
});
</script>
<style lang="less" scoped>
.productDetail_page{
padding-left: 28px;
height: 100%;
.productDetail_page_cotent{
width: 100%;
min-height: 100%;
background: #fff;
.productDetail_page_cotent_center{
width: 1150px;
padding: 20px 70px 30px 30px;
margin: 0 auto;
height: 100%;
}
.detail_item_block{
margin-bottom: 15px;
.detail_icon{
color: #80B8F8;
font-size: 24px;
margin-right: 14px;
}
.detail_item_title_block{
display: flex;
align-items: center;
font-size: 16px;
font-family: Adobe Heiti Std;
font-weight: normal;
color: #030303;
margin-bottom: 10px;
}
.detail_item_content{
padding-left: 10px;
width: 100%;
position: relative;
}
.produce_label_item{
padding: 0 11px;
margin: 9px 27px 9px 0;
height: 27px;
line-height: 27px;
box-sizing: border-box;
border-radius: 4px;
font-size: 16px;
color: #fff;
display: inline-block;
vertical-align: top;
}
.productCategory_block{
width: 100%;
line-height: 46px;
padding-left: 20px;
font-size: 18px;
color: #030303;
height: 46px;
border: 1px solid #DFDFDF;
}
.detail_item_sec_title{
padding-left: 38px;
font-size: 16px;
font-family: Adobe Heiti Std;
font-weight: normal;
color: #030303;
margin-bottom: 10px;
}
.sec_title_margin{
margin: 10px 0;
}
.attribute_block{
background: #F7F8FC;
border: 1px solid #DFDFDF;
padding-top: 30px;
.attribute_block_item{
padding: 0 22px;
display: inline-block;
margin-bottom: 30px;
width: 50%;
.attribute_block_item_content{
display: flex;
align-items: center;
.attribute_item_title{
width: 120px;
margin-right: 20px;
}
.attribute_item_value{
width: 200px;
height: 36px;
line-height: 36px;
border: 1px solid #DFDFDF;
border-radius: 4px;
font-size: 16px;
padding-left: 18px;
color: #030303;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
}
}
.null_data_block{
background: #F7F8FC;
border: 1px solid #DFDFDF;
padding: 30px 0;
text-align: center;
.null_data_img{
width: 130px;
}
}
.product_match_block{
.product_match_item{
background: #F7F8FC;
border: 1px solid #DFDFDF;
padding: 0 16px 26px;
margin-bottom: 20px;
.product_match_item_header{
height: 83px;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 18px;
color: #030303;
}
.product_match_item_content{
display: flex;
.match_item_img_content{
margin-right: 20px;
width: 160px;
}
.match_item_img_block{
display: flex;
align-items: center;
justify-content: center;
width: 160px;
height: 100px;
position: relative;
border: 1px solid #DFDFDF;
cursor: pointer;
img{
max-width: 100%;
max-height: 100%;
}
&:last-child{
margin-right: 0;
}
}
.product_price{
text-align: center;
margin-top: 5px;
color: #030303;
}
}
}
}
.show_more_button{
margin: 20px auto;
display: block;
}
}
.form_width_100{
width: 100%;
}
.product_master_diagram{
width: 400px;
height: 400px;
margin: 30px auto;
display: flex;
align-items: center;
justify-content: center;
.product_master_img{
max-width: 100%;
max-height: 100%;
}
}
}
}
</style>

View File

@@ -0,0 +1,580 @@
<template>
<div class="product_manage_page list_page">
<div class="list_page_content">
<div class="list_top_content">
<div class="list_top_search_content">
<div class="search_content_left">
<filterComponent :title="'Product Label'">
<a-select v-model:value="productLabelIds" size="large" style="width:280px" optionFilterProp="label" :options="productLabelList" placeholder="Please select" allowClear show-search></a-select>
</filterComponent>
<filterComponent :title="'Category'">
<a-select v-model:value="labelItem" size="large" style="width:280px" :options="labelItemList" placeholder="Please select" allowClear @change="labelItemChange" optionFilterProp="label" show-search></a-select>
</filterComponent>
<filterComponent :title="'On Sale State'">
<a-select v-model:value="onSaleState" size="large" style="width:280px" :options="onSaleStateList" placeholder="Please select" allowClear optionFilterProp="label" show-search></a-select>
</filterComponent>
<filterComponent :title="'Label attributes'">
<a-select v-model:value="labelTypeMap" size="large" style="width:280px" :options="labelTypeList" placeholder="Please select" allowClear @change="labelTypeChange" optionFilterProp="label" show-search></a-select>
</filterComponent>
<filterComponent :title="'Add Time'"><a-range-picker v-model:value="addTime" size="large" :placeholder="['Start Time', 'End Time']" format="YYYY-MM-DD" valueFormat="YYYY-MM-DD"/></filterComponent>
<filterComponent :title="'Store'">
<a-select v-model:value="storeIds" size="large" style="width:280px" :options="shopList" placeholder="Please select" allowClear optionFilterProp="label" show-search></a-select>
</filterComponent>
</div>
</div>
<div class="list_top_button_content" :style="{width: '255px'}">
<a-button class="primary_button btn-margin-r-20" type="primary" size="large" @click="searchList()">Seach</a-button>
<a-button class="default_button btn-margin-r-20" size="large" @click="resetList()">Reset</a-button>
<a-button class="primary_button btn-margin-t-35 btn-margin-r-20" type="primary" size="large" @click="addProduct()" v-if="operatePermission.ADD">+Add product</a-button>
<a-button class="primary_button btn-margin-t-35" type="primary" size="large" @click="exportProduct()" :loading="exportLoading">Export</a-button>
</div>
</div>
<div class="list_table_content">
<a-table :columns="columns" :data-source="collectionList" @change="changePage" :loading="tableLoading"
:getPopupContainer="(triggerNode) => getPopupContainer(triggerNode)"
:pagination="{
showSizeChanger:true,
current: currentPage,
pageSize:pageSize,
total: total,
showQuickJumper:true,
bordered:false,
pageSizeOptions:['10','20','50'],
}">
<template v-slot:bodyCell="{column,record, index}" >
<template v-if="column.dataIndex === 'pictureUrl'">
<img class="product_pic" :src="record.pictureUrl">
</template>
<template v-if="column.dataIndex === 'productLabel'">
<a-popover placement="top" :getPopupContainer="(triggerNode) => getPopupContainer(triggerNode)" arrowPointAtCenter>
<template #content>
<div class="record_pop_block">{{record.productLabel}}</div>
</template>
<div class="record_pop_content">{{record.productLabel}}</div>
</a-popover>
</template>
<template v-if="column.dataIndex === 'labelType'">
<a-popover placement="top" :getPopupContainer="(triggerNode) => getPopupContainer(triggerNode)">
<template #content>
<div class="record_pop_block">{{record.labelType}}</div>
</template>
<div class="record_pop_content">{{record.labelType}}</div>
</a-popover>
</template>
<template v-if="column.dataIndex === 'shop'" >
<a-popover placement="top" :getPopupContainer="(triggerNode) => getPopupContainer(triggerNode)">
<template #content>
<div class="record_pop_block">{{record.shop}}</div>
</template>
<div class="record_pop_content">{{record.shop}}</div>
</a-popover>
</template>
<template v-if="column.dataIndex === 'onSaleState'">
<a-switch checked-children="open" un-checked-children="close" v-model:checked="record.onSaleState" @change="change_on_sale(record)"/>
</template>
<template v-if="column.dataIndex === 'operation'">
<div class="operate_list">
<div class="operate_item" @click="editProduct(record)" v-if="operatePermission.EDIT">Edit</div>
<div class="operate_item" @click="toProductDteail(record)" v-if="operatePermission.DETAIL">Detail</div>
<div class="operate_item" @click="deleteProduct(record,index)" v-if="operatePermission.DELETE">Delete</div>
</div>
</template>
</template>
</a-table>
</div>
</div>
<AddProduct ref="addproductComponent" :productLabelList="productLabelList" @confirmSubmitAdd="confirmSubmitAdd"></AddProduct>
<EditProduct ref="editProductComponent" :productLabelList="productLabelList" @refreshList="getProductlist"></EditProduct>
</div>
</template>
<script lang="ts">
import { defineComponent,ref,reactive,toRefs,UnwrapRef,onMounted,createVNode,computed } from "vue";
import { useRouter,useRoute } from 'vue-router'
import filterComponent from '@/component/filterComponent.vue'
import AddProduct from '@/component/productComponent/addProduct.vue'
import EditProduct from '@/component/productComponent/editProduct.vue'
import { Moment } from 'moment';
import { message,Modal } from "ant-design-vue";
import { WarningOutlined } from '@ant-design/icons-vue';
import { Https } from "@/tool/https";
import { formatTime, startTime, endTime } from "@/tool/util"
type TableDataType = {
key: string;
name: string;
age: number;
createDate: string;
};
export default defineComponent({
name:'productManage',
components:{filterComponent,AddProduct,EditProduct},
setup(){
const router = useRouter()
const route = useRoute()
let filter:any = reactive({
productLabelIds:[],
labelItem:[],
onSaleState:[],
labelTypeMap:[],
addTime:ref<Moment[]>([]),
storeIds:[],
})
let filterLabelTypeMap :any = ref({})
let tableLoading = ref(false)
let exportLoading = ref(false)
let addproductComponent:any = ref(null)
let editProductComponent:any = ref(null)
let productLabelList:any = ref([])
let labelItemList:any = ref([])
let labelTypeList:any = ref([])
let onSaleStateList:any = ref([{
value:'1',label:'Open'
},{
value:'0',label:'Close'
}])
let shopList:any = ref([])
const columns = computed(() => {
const sorted = sortedInfo.value || {};
return [
{ title: 'Product Pic', align:'center', ellipsis: true, dataIndex: 'pictureUrl', key: 'pictureUrl' },
{ title: 'Product Label', align:'center', dataIndex: 'productLabel', key: 'productLabel'},
{ title: 'Category', align:'center', ellipsis: true, dataIndex: 'labelItem', key: 'labelItem'},
{ title: 'Label attributes', align:'center', dataIndex: 'labelType', key: 'labelType'},
{ title: 'Shop', align:'center', dataIndex: 'shop', key: 'shop' },
{ title: 'Inventory', align:'center', ellipsis: true, dataIndex: 'total', key: 'total', sorter:()=>{},
sortOrder: sorted.columnKey === 'total' && sorted.order,
},
{ title: 'price', align:'center', ellipsis: true, dataIndex: 'price', key: 'price',customRender:(record:any)=>{
let newPrrice = '$' + record.text
return newPrrice },
sorter:()=>{},
sortOrder: sorted.columnKey === 'price' && sorted.order,
},
{ title: 'Add Time', align:'center', ellipsis: true, dataIndex: 'createDate', key: 'createDate',customRender:(record:any)=>{
let time = formatTime(record.text / 1000, 'YYYY-MM-DD hh:mm:ss')
return time },
sorter:()=>{},
sortOrder: sorted.columnKey === 'createDate' && sorted.order,
},
{ title: 'On Sale State', align:'center', ellipsis: true, dataIndex: 'onSaleState', key: 'onSaleState' },
{
title: 'Operations',
key: 'operation',
align:'center',
fixed: 'right',
width: 150,
dataIndex:'operation',
},
]
})
let collectionList = ref([])
let currentPage = ref(1)
let pageSize = ref(10)
let total = ref(1)
let editProductTitle = ref('Add Product')
let formState = ref({
productId: '',
productName: '',
productType:'',
remarks:'',
});
let columnSortListData:any = []; //排序参数
let sortedInfo:any = ref()
let typeMap:any = {
dress:'Dress_Type',
top:'Top_Type',
bottom:'BTM_Type',
outer:'Outer_Type',
jumpsuit:'Jumpsuit_subtype'
}
let rules = {
productName: [
{ required: true, message: 'Please input product name', trigger: 'blur' },
],
productType: [
{ required: true, message: 'Please select product type', trigger: 'blur' },
],
}
let operatePermission:any = reactive({
DETAIL:false,
ADD:false,
EDIT:false,
DELETE:false,
})
let getPopupContainer = (triggerNode:any) =>{
return triggerNode.parentNode || document.body
}
let changePage = (e:any, filters: any, sorter: any) =>{
currentPage.value = e.current
pageSize.value = e.pageSize
if(sorter.order){
columnSortListData = [{
column:getSortField(sorter.field),
style:sorter.order == 'ascend' ? 'asc' : 'desc'
}]
sortedInfo.value = sorter
}else{
columnSortListData = []
sortedInfo.value = sorter
}
getProductlist()
}
let getSortField = (field:string) =>{
let fieldData:any = {
createDate: 'create_date',
price:'price',
total:'total',
}
return fieldData[field]
}
let closeProduct = () =>{
formState.value = {
productId: '',
productName: '',
productType:'',
remarks:'',
}
}
let addProduct = () =>{
addproductComponent.value.showModal()
}
let change_on_sale = (record:any) =>{
let data = {
id:record.id,
onSaleState:record.onSaleState ? 1 : 0
}
Https.axiosPost(Https.httpUrls.doOnSale, data).then(
(rv: any) => {
if (rv) {
}
}
);
}
let editProduct = (record:any) =>{
editProductComponent.value.showModal({record:record,labelItemList:labelItemList.value})
}
let toProductDteail = (record:any) =>{
router.push({path:'/home/productDetail',query:{id:record.id}})
}
let getProductlist = () =>{
let data = {
columnSortList:columnSortListData,
productLabelIds:filter.productLabelIds && filter.productLabelIds.length ? [filter.productLabelIds] : [],
labelItem:filter.labelItem && filter.labelItem.length ? filter.labelItem : '',
onSaleState:filter.onSaleState && filter.onSaleState.length ? filter.onSaleState : '',
labelTypeMap:filterLabelTypeMap.value,
storeIds:filter.storeIds && filter.storeIds.length ? [filter.storeIds] : [],
createDateStart:filter.addTime && filter.addTime.length ? startTime(filter.addTime[0]) : '',
createDateEnd:filter.addTime && filter.addTime.length ? endTime(filter.addTime[1]) : '',
page:currentPage.value,
size:pageSize.value
}
tableLoading.value = true
Https.axiosPost(Https.httpUrls.queryProductPage, data).then(
(rv: any) => {
if (rv) {
tableLoading.value = false
collectionList.value = rv.content.map((v:any)=>{
return {
...v,
onSaleState:v.onSaleState == 1? true :false,
productLabel:getProductLabel(v.productLabelInfo),
labelItem:v.attributeItemInfo.labelItem,
labelType:getLabelType(v.attributeItemInfo || []),
shop:getStoreName(v.storeInfoList)
}
})
getProductPromission()
total.value = rv.total
}
}
).catch((err:any)=>{
tableLoading.value = false
})
}
let getProductLabel = (productLabelInfo:any) =>{
let productLabel = productLabelInfo.map((v:any) => v.name).join(',')
return productLabel
}
let getLabelType = (attributeItemInfo:any) => {
// let labelType = attributeItemInfo.attributeTypeList ? attributeItemInfo.attributeTypeList.filter((v:any) => v.labelType === typeMap[attributeItemInfo.labelItem]) :[]
// labelType = labelType.map(((v:any)=>v.attributeValueList)).join(',')
let labelType = attributeItemInfo.labelTypeValueList && attributeItemInfo.labelTypeValueList.length ? attributeItemInfo.labelTypeValueList.join(',') : ''
return labelType
}
let getStoreName = (storeInfoList:any) =>{
let storeName = storeInfoList.map((v:any) => v.storeName).join(',')
return storeName
}
let getProductLabelList = () =>{
Https.axiosPost(Https.httpUrls.queryProductLabel).then(
(rv: any) => {
if (rv) {
productLabelList.value = rv.map((v:any)=>{
return {
...v,
value:v.id,
label:v.name
}
})
}
}
);
}
let getStoreList = () =>{
Https.axiosPost(Https.httpUrls.queryProductStore).then(
(rv: any) => {
if (rv) {
shopList.value = rv.map((v:any)=>{
return {
...v,
value:v.id,
label:v.name
}
})
}
}
);
}
let getAttributeList = () =>{
Https.axiosGet(Https.httpUrls.attributeQueryAll).then(
(rv: any) => {
if (rv) {
labelItemList.value = rv.map((v:any)=>{
return {
...v,
value:v.labelItem,
label:v.labelItem
}
})
}
}
);
}
let labelItemChange = (event:any,selectedOptions:any) =>{
filter.labelTypeMap =[]
filterLabelTypeMap.value = {}
if(selectedOptions){
labelTypeList.value = selectedOptions.labelTypeValueList.map((v:any)=>{
return {
value:v,
label:v
}
})
}else{
labelTypeList.value = []
}
}
let labelTypeChange = (event:any,selectedOptions:any) => {
let key = typeMap[filter.labelItem]
if(selectedOptions){
filterLabelTypeMap.value[key] = [event]
}else{
filterLabelTypeMap.value = {}
}
}
let searchList = () =>{
currentPage.value = 1
getProductlist()
}
let exportProduct = () =>{
let data = {
columnSortList:columnSortListData,
productLabelIds:filter.productLabelIds && filter.productLabelIds.length ? [filter.productLabelIds] : [],
labelItem:filter.labelItem && filter.labelItem.length ? filter.labelItem : '',
onSaleState:filter.onSaleState && filter.onSaleState.length ? filter.onSaleState : '',
labelTypeMap:filterLabelTypeMap.value,
storeIds:filter.storeIds && filter.storeIds.length ? [filter.storeIds] : [],
createDateStart:filter.addTime && filter.addTime.length ? startTime(filter.addTime[0]) : '',
createDateEnd:filter.addTime && filter.addTime.length ? endTime(filter.addTime[1]) : '',
}
exportLoading.value = true
Https.axiosPost(Https.httpUrls.exportProduct, data).then(
(rv: any) => {
if (rv) {
exportLoading.value = false
window.location.href= rv
}
}
).catch((err:any)=>{
exportLoading.value = false
})
}
let resetList = () =>{
currentPage.value = 1
filter.productLabelIds = []
filter.labelItem = []
filter.onSaleState = []
filter.labelTypeMap = []
filter.addTime = ref<Moment[]>([])
filter.storeIds = []
sortedInfo.value = null
columnSortListData = []
getProductlist()
}
let deleteProduct = (data:any,index:any) =>{
let confirmDelete = (data:any,index:any) =>{
let newData = {
id:data.id
}
Https.axiosPost(Https.httpUrls.productDelete, newData).then(
(rv: any) => {
if (rv) {
message.success('Delete success')
collectionList.value.splice(index,1)
}
}
);
}
Modal.confirm({
title: "Are you sure about the product? The product associated with the product will become invalid after deletion!",
icon: createVNode(WarningOutlined),
class:'confirm_style',
okText: 'Ok',
cancelText: 'Cancel',
// centered:true,
onOk() {
confirmDelete(data,index)
}
});
}
let getProductPromission = () =>{
let menuList:any = sessionStorage.getItem('menuList')
menuList= JSON.parse(menuList)
for(let item of menuList){
if(item.name === 'Product Management'){
for(let operate of item.operationList){
operatePermission[operate.code] = operate.select == 1 ? true :false
}
}
}
}
let confirmSubmitAdd = () =>{
resetList()
}
onMounted(() => {
if(route.query && route.query.type === 'add'){
addProduct()
}
getProductlist()
getProductLabelList()
getStoreList()
getAttributeList()
})
return {
...toRefs(filter),
addproductComponent,
editProductComponent,
tableLoading,
exportLoading,
productLabelList,
labelItemList,
labelTypeList,
onSaleStateList,
shopList,
columns,
collectionList,
currentPage,
pageSize,
total,
editProductTitle,
formState,
rules,
operatePermission,
getPopupContainer,
changePage,
closeProduct,
addProduct,
change_on_sale,
editProduct,
toProductDteail,
labelItemChange,
labelTypeChange,
searchList,
exportProduct,
resetList,
deleteProduct,
confirmSubmitAdd,
getProductlist
}
},
});
</script>
<style lang="less" scoped>
.product_manage_page{
padding-left: 28px;
.product_pic{
width: 50px;
}
.operate_list{
display: flex;
justify-content: space-between;
.operate_item{
font-size: 14px;
font-family: Roboto;
font-weight: 400;
color: #343579;
cursor: pointer;
}
}
.record_pop_block{
max-width: 300px;
// word-break: break-all;
}
.record_pop_content{
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
}
</style>

View File

@@ -0,0 +1,292 @@
<template>
<div class="store_manage_page list_page">
<div class="list_page_content">
<div class="list_top_content">
<div class="list_top_search_content">
<div class="search_content_left">
<filterComponent :title="'Store Name'"><a-input v-model:value="name" size="large" placeholder="Please input Store Name or Id" @keydown.enter="searchList"/></filterComponent>
</div>
</div>
<div class="list_top_button_content">
<a-button class="primary_button btn-margin-r-20" type="primary" size="large" @click="searchList">Seach</a-button>
<a-button class="default_button" size="large" @click="resetList">Reset</a-button>
<a-button class="primary_button btn-margin-t-35" type="primary" size="large" @click="addStore()">+Add Store</a-button>
</div>
</div>
<div class="list_table_content">
<a-table :columns="columns" :data-source="collectionList" @change="changePage" :loading="tableLoading"
:pagination="{
showSizeChanger:true,
current: currentPage,
pageSize:pageSize,
total: total,
showQuickJumper:true,
bordered:false,
pageSizeOptions:['10','20','50'],
}">
<template v-slot:bodyCell="{column,record, index}" >
<template v-if="column.dataIndex === 'operation'">
<div class="operate_list">
<div class="operate_item" @click="editStore(record)">Edit</div>
<div class="operate_item" @click="deleteStore(record,index)">Delete</div>
</div>
</template>
</template>
</a-table>
</div>
</div>
<a-modal class="edit_modal_component"
:destroyOnClose="true"
v-model:visible="editStoreModal"
:footer="null"
:title="editStoreTitle"
width="560px"
:maskClosable="false"
:centered="true"
@cancel="closeStore"
>
<a-form ref="formRef" :model="formState" :rules="rules" :layout="'vertical'" >
<a-form-item label="Store Name" name="name">
<a-input v-model:value="formState.name" size="large" placeholder="Please input store name"/>
</a-form-item>
<a-form-item label="Store Adress" name="address">
<a-input v-model:value="formState.address" size="large" placeholder="Please select"/>
</a-form-item>
<a-form-item label="Remarks" name="remarks">
<a-input v-model:value="formState.remarks" size="large" placeholder="Please input remarks"/>
</a-form-item>
</a-form>
<div class="modal_button_list">
<a-button class="default_button btn-margin-r-20" size="large" @click="closeStore">Cancel</a-button>
<a-button class="primary_button" type="primary" size="large" @click="confirmSubmit">Submit</a-button>
</div>
</a-modal>
</div>
</template>
<script lang="ts">
import { defineComponent,ref,reactive,toRefs,UnwrapRef, onMounted,createVNode } from "vue";
import filterComponent from '@/component/filterComponent.vue'
import { message,Modal } from "ant-design-vue";
import { formatTime } from "@/tool/util"
import { Https } from "@/tool/https";
import { useRoute } from 'vue-router'
import { WarningOutlined } from '@ant-design/icons-vue';
export default defineComponent({
name:'storeManage',
components:{filterComponent},
setup(){
const route = useRoute()
let filter = reactive({
name:''
})
let columns = reactive([
{ title: 'Store Id', align:'center', ellipsis: true, dataIndex: 'id', key: 'id' },
{ title: 'Store Name', align:'center', ellipsis: true, dataIndex: 'name', key: 'name',
},
{ title: 'Store Adress', align:'center', ellipsis: true, dataIndex: 'address', key: 'address' },
{ title: 'Remarks', align:'center', ellipsis: true, dataIndex: 'remarks', key: 'remarks' },
{ title: 'Add Time', align:'center', ellipsis: true, dataIndex: 'createDate', key: 'createDate',customRender:(record:any)=>{
let time = formatTime(record.text / 1000, 'YYYY-MM-DD hh:mm:ss')
return time },
},
{
title: 'Operations',
key: 'operation',
align:'center',
fixed: 'right',
width: 150,
dataIndex:'operation',
},
])
let tableLoading = ref(false)
let collectionList = ref([])
let currentPage = ref(1)
let pageSize = ref(10)
let total = ref(0)
let editStoreModal = ref(false)
let editStoreTitle = ref('Add Store')
let formRef = ref()
let formState = ref({
id: '',
name: '',
address:'',
remarks:'',
});
let rules = {
name: [
{ required: true, message: 'Please input store name', trigger: 'blur' },
],
address: [
{ required: true, message: 'Please input store address', trigger: 'blur' },
],
}
let changePage = (e:any) =>{
currentPage.value = e.current
pageSize.value = e.pageSize
getStoreList()
}
let closeStore = () =>{
formState.value = {
id: '',
name: '',
address:'',
remarks:'',
}
editStoreModal.value = false
}
let addStore = () =>{
editStoreTitle.value = 'Add Store'
editStoreModal.value = true
}
let editStore = (data:any,) =>{
editStoreModal.value = true
editStoreTitle.value = 'Eidt Store'
formState.value = {
id: data.id,
name: data.name,
address:data.address,
remarks:data.remarks,
}
}
let getStoreList = () =>{
let data = {
name : filter.name,
page:currentPage.value,
size:pageSize.value
}
tableLoading.value = true
Https.axiosPost(Https.httpUrls.queryStorePage, data).then(
(rv: any) => {
if (rv) {
tableLoading.value = false
collectionList.value = rv.content
total.value = rv.total
}
}
);
}
let searchList = () =>{
currentPage.value = 1
getStoreList()
}
let resetList = () =>{
currentPage.value = 1
filter.name = ''
getStoreList()
}
let confirmSubmit = () =>{
let data = {
...formState.value,
timeZone:Intl.DateTimeFormat().resolvedOptions().timeZone,
}
formRef.value.validate().then(()=>{
submit()
})
let submit = () =>{
Https.axiosPost(Https.httpUrls.sotreSaveOrEdit, data).then(
(rv: any) => {
if (rv) {
let tip = !formState.value.id ? 'Store added successfully' :'Edit store successfully'
message.success(tip)
resetList()
closeStore()
}
}
);
}
}
let deleteStore = (data:any,index:any) => {
let confirmDelete = (data:any,index:any) =>{
let newData = {
id:data.id
}
Https.axiosPost(Https.httpUrls.storeDelete, newData).then(
(rv: any) => {
if (rv) {
message.success('Delete success')
collectionList.value.splice(index,1)
}
}
);
}
Modal.confirm({
title: 'Are you sure to delete the store?',
icon: createVNode(WarningOutlined),
class:'confirm_style',
okText: 'Yes',
cancelText: 'No',
// centered:true,
onOk() {
confirmDelete(data,index)
}
});
}
onMounted(() => {
if(route.query && route.query.type === 'add'){
addStore()
}
getStoreList()
})
return {
...toRefs(filter),
columns,
collectionList,
currentPage,
tableLoading,
pageSize,
total,
editStoreModal,
editStoreTitle,
formRef,
formState,
rules,
changePage,
closeStore,
addStore,
editStore,
searchList,
resetList,
confirmSubmit,
deleteStore
}
},
});
</script>
<style lang="less" scoped>
.store_manage_page{
padding-left: 28px;
.operate_list{
display: flex;
justify-content: space-between;
.operate_item{
font-size: 14px;
font-family: Roboto;
font-weight: 400;
color: #343579;
cursor: pointer;
}
}
}
</style>

View File

@@ -0,0 +1,469 @@
<template>
<div class="role_manage_page list_page">
<div class="list_page_content">
<div class="list_top_content">
<div class="list_top_search_content">
<div class="search_content_left">
<filterComponent :title="'Role Name or Id'"><a-input v-model:value="name" size="large" placeholder="Please input Role Name or Id" @keydown.enter="searchList()"/></filterComponent>
</div>
</div>
<div class="list_top_button_content">
<a-button class="primary_button btn-margin-r-20" type="primary" size="large" @click="searchList()">Seach</a-button>
<a-button class="default_button" size="large" @click="resetList()">Reset</a-button>
<a-button class="primary_button btn-margin-t-35" type="primary" size="large" @click="addRole()">+Add Role</a-button>
</div>
</div>
<div class="list_table_content">
<a-table :columns="columns" :data-source="collectionList" @change="changePage" :loading="tableLoading"
:pagination="{
showSizeChanger:true,
current: currentPage,
pageSize:pageSize,
total: total,
showQuickJumper:true,
bordered:false,
pageSizeOptions:['10','20','50'],
}">
<template v-slot:bodyCell="{column,record, index}" >
<template v-if="column.dataIndex === 'operation'">
<div class="operate_list">
<div class="operate_item" @click="editRole(record)">Edit</div>
<div class="operate_item" @click="deleteRole(record,index)">Delete</div>
</div>
</template>
</template>
</a-table>
</div>
</div>
<a-modal class="edit_modal_component"
v-model:visible="editRoleModal"
:footer="null"
:title="editRoleTitle"
width="680px"
:maskClosable="false"
:centered="true"
@cancel="closeRole"
>
<a-form ref="formRef" :model="formState" :rules="rules" :layout="'vertical'" >
<a-form-item label="Role Name" name="name">
<a-input v-model:value="formState.name" size="large" placeholder="Please input role name"/>
</a-form-item>
<div>
<div class="form_item_title required">Role Permission</div>
<a-checkbox class="form_check_all" size="large" v-model:checked="permissionCheckAll" @change="perCheckAll">All</a-checkbox>
<table class="permission_table">
<tr v-for="(role) in rolePermission" :key="role.code">
<td class="first_td">
<div class="table_td_block"><a-checkbox size="large" v-model:checked="role.select" @change="checkLevelFirst(role)">{{role.resource}}</a-checkbox></div>
</td>
<td class="second_td">
<div class="table_td_block" v-for="(menu) in role.level2ResourceList" :key="menu.code"><a-checkbox size="large" v-model:checked="menu.select" @change="checkLevelSec(role,menu)">{{menu.resource}}</a-checkbox></div>
</td>
<td class="third_td">
<div class="table_td_block" v-for="(menu) in role.level2ResourceList" :key="menu.code">
<div class="table_td_permission">
<div v-for="(per) in menu.operationList" :key="per.code"><a-checkbox :disabled="!menu.select" size="large" v-model:checked="per.select">{{per.resource}}</a-checkbox></div>
</div>
</div>
</td>
</tr>
</table>
</div>
</a-form>
<div class="modal_button_list">
<a-button class="default_button btn-margin-r-20" size="large" @click="closeRole()">Cancel</a-button>
<a-button class="primary_button" type="primary" size="large" @click="confirmSubmit()">Submit</a-button>
</div>
</a-modal>
</div>
</template>
<script lang="ts">
import { defineComponent,ref,reactive,toRefs,UnwrapRef,onMounted,createVNode } from "vue";
import filterComponent from '@/component/filterComponent.vue'
import { message,Modal } from "ant-design-vue";
import { Https } from "@/tool/https";
import { WarningOutlined } from '@ant-design/icons-vue';
export default defineComponent({
name:'roleManage',
components:{filterComponent},
setup(){
let filter = reactive({
name:'',
})
let tableLoading = ref(false)
let columns = reactive([
{ title: 'Role Name', align:'center', ellipsis: true, dataIndex: 'name', key: 'name', width:250 },
{ title: 'Role Permission', align:'center', ellipsis: true, dataIndex: 'permission', key: 'permission'},
{
title: 'Operations',
key: 'operation',
align:'center',
fixed: 'right',
width: 250,
dataIndex:'operation',
},
])
let collectionList = ref([])
let currentPage = ref(1)
let pageSize = ref(10)
let total = ref(1)
let editRoleModal = ref(false)
let editRoleTitle = ref('Add Role')
let permissionCheckAll=ref(false)
let rolePermission:any = ref([])
let backupRolePermission:any = ref([])
let formRef = ref();
let formState = ref({
id: '',
name: '',
});
let rules = {
name: [
{ required: true, message: 'Please input role name', trigger: 'blur' },
],
roleType: [
{ required: true, message: '', trigger: 'blur' },
],
}
let changePage = (e:any) =>{
currentPage.value = e.current
pageSize.value = e.pageSize
getRoleList()
}
let closeRole = () =>{
formState.value = {
id: '',
name: '',
}
permissionCheckAll.value = false
rolePermission.value = JSON.parse(JSON.stringify(backupRolePermission.value))
editRoleModal.value = false
}
let addRole = () =>{
editRoleTitle.value = 'Add Role'
editRoleModal.value = true
}
let editRole = (data:any,) =>{
editRoleModal.value = true
editRoleTitle.value = 'Eidt Role'
formState.value = {
id: data.id,
name: data.name,
}
rolePermission.value = changeSelectBoolean(data.rolePermission.level1ResourceList)
permissionCheckAll.value = data.rolePermission.allSelect
}
let getRoleList = () =>{
let data = {
name : filter.name,
page:currentPage.value,
size:pageSize.value
}
tableLoading.value = true
Https.axiosPost(Https.httpUrls.roleQueryRolePage, data).then(
(rv: any) => {
if (rv) {
tableLoading.value = false
collectionList.value = rv.content.map((v:any) =>{
let data = {
...v,
permission:v.rolePermission.level1ResourceList.filter((v:any)=>v.select).map((per:any) => per.resource).join('、')
}
return data
})
total.value = rv.total
}
}
);
}
let getMenuListPer = () =>{
Https.axiosGet(Https.httpUrls.roleQueryPermissionList).then(
(rv: any) => {
if (rv) {
rolePermission.value = changeSelectBoolean(rv)
backupRolePermission.value = changeSelectBoolean(rv)
console.log(rolePermission.value);
}
}
);
}
let changeSelectBoolean = (data:any) =>{
let newData = JSON.parse(JSON.stringify(data))
for(let first of newData){
first.select = changeBooleanNumber(first.select)
for(let sec of first.level2ResourceList){
sec.select = changeBooleanNumber(sec.select)
for(let third of sec.operationList){
third.select = changeBooleanNumber(third.select)
}
}
}
return newData
}
//判断是布尔值则转为数字,数字则转布尔值
let changeBooleanNumber = (data:any) => {
let newData = data
if(newData.constructor === Boolean){
newData = newData ? 1 :0
}else{
newData = newData ? true :false
}
return newData
}
let searchList = () =>{
currentPage.value = 1
getRoleList()
}
let resetList = () =>{
currentPage.value = 1
filter.name = ''
getRoleList()
}
let deleteRole = (data:any,index:any) =>{
let confirmDelete = (data:any,index:any) =>{
let newData = {
id:data.id
}
Https.axiosPost(Https.httpUrls.roleDelete, newData).then(
(rv: any) => {
if (rv) {
message.success('Delete success')
collectionList.value.splice(index,1)
}
}
);
}
Modal.confirm({
title: "Are you sure delete the role?",
icon: createVNode(WarningOutlined),
class:'confirm_style',
okText: 'Ok',
cancelText: 'Cancel',
// centered:true,
onOk() {
confirmDelete(data,index)
}
});
}
let perCheckAll = () =>{
for(let first of rolePermission.value){
first.select = permissionCheckAll.value
for(let sec of first.level2ResourceList){
sec.select = permissionCheckAll.value
for(let third of sec.operationList){
third.select = permissionCheckAll.value
}
}
}
}
let checkLevelFirst = (firstDdata:any) =>{
for(let sec of firstDdata.level2ResourceList){
sec.select = firstDdata.select
for(let third of sec.operationList){
third.select = firstDdata.select
}
}
checkIsAllPer()
}
let checkLevelSec = (firstData:any, secondData:any) =>{
for(let third of secondData.operationList){
third.select = secondData.select
}
let status = false
for(let item of firstData.level2ResourceList){
if(item.select){
status = true
break
}
}
firstData.select = status
checkIsAllPer()
}
//判断是否已经全部勾选
let checkIsAllPer = () =>{
let status = true
for(let first of rolePermission.value){
if(!first.select){
status = false
break
}
for(let sec of first.level2ResourceList){
if(!sec.select){
status = false
break
}
}
if(!status){break}
}
permissionCheckAll.value = status
}
let confirmSubmit = () =>{
let data = {
...formState.value,
rolePermission:{
allSelect:permissionCheckAll.value ? 1 : 0,
level1ResourceList:changeSelectBoolean(rolePermission.value)
},
timeZone:Intl.DateTimeFormat().resolvedOptions().timeZone,
}
formRef.value.validate().then(()=>{
let status = false
for(let first of rolePermission.value){
if(first.select){
status = true
break
}
for(let sec of first.level2ResourceList){
if(sec.select){
status = true
break
}
}
if(status){break}
}
if(!status){
message.error('Please check role permission')
return
}
submit()
})
let submit = () => {
Https.axiosPost(Https.httpUrls.roleSaveOrEdit, data).then(
(rv: any) => {
if (rv) {
let tip = !formState.value.id ? 'Role added successfully' :'Edit role successfully'
message.success(tip)
resetList()
closeRole()
}
}
);
}
}
onMounted(() => {
getRoleList()
getMenuListPer()
})
return {
...toRefs(filter),
tableLoading,
columns,
collectionList,
currentPage,
pageSize,
total,
permissionCheckAll,
rolePermission,
editRoleModal,
editRoleTitle,
formRef,
formState,
rules,
changePage,
closeRole,
addRole,
editRole,
searchList,
resetList,
deleteRole,
perCheckAll,
checkLevelFirst,
checkLevelSec,
confirmSubmit,
}
},
});
</script>
<style lang="less" scoped>
.role_manage_page{
padding-left: 28px;
.role_pic{
width: 50px;
}
.operate_list{
display: flex;
justify-content: space-between;
padding: 0 50px;
.operate_item{
font-size: 14px;
font-family: Roboto;
font-weight: 400;
color: #343579;
cursor: pointer;
}
}
}
.form_check_all{
margin: 10px 0;
}
.permission_table{
width: 100%;
margin-top: 10px;
border-left:1px solid #DFDFDF;
border-top:1px solid #DFDFDF;
td{
border-bottom: 1px solid #DFDFDF;
border-right: 1px solid #DFDFDF;
.table_td_block{
padding: 16px 0 16px 20px;
border-bottom: 1px solid #DFDFDF;
&:last-child{
border-bottom: none;
}
}
.table_td_permission{
display: flex;
min-height: 23px;
}
}
.first_td{
width: 120px;
}
.second_td{
width: 150px;
}
}
</style>

View File

@@ -0,0 +1,513 @@
<template>
<div class="user_manage_page list_page">
<div class="list_page_content">
<div class="list_top_content">
<div class="list_top_search_content">
<div class="search_content_left">
<filterComponent :title="'User Name'"><a-input v-model:value="userName" size="large" placeholder="Please input User Name" @keydown.enter="searchList()"/></filterComponent>
<filterComponent :title="'User Phone'"><a-input v-model:value="userPhone" size="large" placeholder="Please input User Phone" @keydown.enter="searchList()" /></filterComponent>
<filterComponent :title="'User Role'">
<a-select v-model:value="roleId" size="large" style="width:280px" placeholder="Please select" :options="userRoleList" allowClear></a-select>
</filterComponent>
<filterComponent :title="'User State'">
<a-select v-model:value="state" size="large" style="width:280px" placeholder="Please select" :options="userStateList" allowClear></a-select>
</filterComponent>
<filterComponent :title="'Add Time'"><a-range-picker v-model:value="addTime" size="large" :placeholder="['Start Time', 'End Time']" format="YYYY-MM-DD" valueFormat="YYYY-MM-DD"/></filterComponent>
</div>
</div>
<div class="list_top_button_content">
<a-button class="primary_button btn-margin-r-20" type="primary" size="large" @click="searchList()">Seach</a-button>
<a-button class="default_button" size="large" @click="resetList()">Reset</a-button>
<a-button class="primary_button btn-margin-t-35" type="primary" size="large" @click="addUser()">+Add User</a-button>
</div>
</div>
<div class="list_table_content">
<a-table :columns="columns" :data-source="collectionList" @change="changePage" :loading="tableLoading"
:pagination="{
showSizeChanger:true,
current: currentPage,
pageSize:pageSize,
total: total,
showQuickJumper:true,
bordered:false,
pageSizeOptions:['10','20','50'],
}">
<template v-slot:bodyCell="{column,record, index}" >
<template v-if="column.dataIndex === 'stateName'">
<div :class="['state_name_block', record.state == 1 ? 'enable_state' :'disable_state']">
<div class="state_round"></div>
<div>{{record.stateName}}</div>
</div>
</template>
<template v-if="column.dataIndex === 'operation'">
<div class="operate_list">
<div class="operate_item" @click="editUser(record)">Edit</div>
<div :class="['operate_item', record.state == 0 ? '' :'operate_disable_state']" @click="enableUser(record,index)">
<div v-if="record.state == 0">Enable</div>
<div v-else>Disable</div>
</div>
<div class="operate_item" @click="deleteUser(record,index)">Delete</div>
</div>
</template>
</template>
</a-table>
</div>
</div>
<a-modal class="edit_modal_component"
:destroyOnClose="true"
v-model:visible="editUserModal"
:footer="null"
:title="editUserTitle"
width="680px"
:maskClosable="false"
:centered="true"
@cancel="closeUser"
>
<a-form ref="formRef" :model="formState" :rules="rules" :layout="'vertical'" >
<a-row :gutter="[16,16]">
<a-col :span="12">
<a-form-item label="User Name" name="userName">
<a-input v-model:value="formState.userName" size="large" placeholder="Please input user name"/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="User Account" name="userAccount">
<a-input v-model:value="formState.userAccount" size="large" placeholder="Please input user account"/>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="[16,16]">
<a-col :span="12">
<a-form-item label="User Code" name="userPassword">
<a-input v-model:value="formState.userPassword" size="large" placeholder="Please input user code" type="password"/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="User Phone" name="userPhone">
<a-input v-model:value="formState.userPhone" size="large" placeholder="Please input user phone"/>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="[16,16]">
<a-col :span="12">
<a-form-item label="App User" name="appUser">
<a-select v-model:value="formState.appUser" size="large" :options="appUserList" placeholder="Please select"></a-select>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="User Role" name="roleId">
<a-select v-model:value="formState.roleId" size="large" :options="userRoleList" placeholder="Please select"></a-select>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="[16,16]">
<a-col :span="24">
<a-form-item label="Owner Store" name="storeIds">
<a-select v-model:value="formState.storeIds" size="large" :options="ownerStoreList" mode="multiple" placeholder="Please select"></a-select>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="[16,16]">
<a-col :span="24">
<a-form-item label ="Remarks" name="remarks">
<a-input v-model:value="formState.remarks" size="large" placeholder="Please input remarks"/>
</a-form-item>
</a-col>
</a-row>
</a-form>
<div class="modal_button_list">
<a-button class="default_button btn-margin-r-20" size="large" @click="closeUser">Cancel</a-button>
<a-button class="primary_button" type="primary" size="large" @click="confirmSubmit">Submit</a-button>
</div>
</a-modal>
</div>
</template>
<script lang="ts">
import { defineComponent,ref,reactive,toRefs,UnwrapRef,onMounted,createVNode } from "vue";
import filterComponent from '@/component/filterComponent.vue'
import { Moment } from 'moment';
import { message,Modal } from "ant-design-vue";
import { Https } from "@/tool/https";
import { WarningOutlined } from '@ant-design/icons-vue';
import { encode, decode } from 'js-base64';
import { useRoute } from 'vue-router'
import { formatTime, startTime, endTime } from "@/tool/util"
export default defineComponent({
name:'userManage',
components:{filterComponent},
setup(){
const route = useRoute()
let filter:any = reactive({
userName:'',
userPhone:'',
roleId:[],
state:[],
addTime:ref<Moment[]>([]),
})
let tableLoading = ref(false)
let userRoleList:any = ref([])
let userStateList:any = ref([
{value:'1',label:'Activated'},
{value:'0',label:'Deactivated'},
])
let appUserList:any = ref([
{value:1,label:'Yes'},
{value:0,label:'No'},
])
let ownerStoreList:any = ref([])
let columns = reactive([
{ title: 'User Name', align:'center', ellipsis: true, dataIndex: 'userName', key: 'userName' },
{ title: 'User Account', align:'center', ellipsis: true, dataIndex: 'userAccount', key: 'userAccount'},
{ title: 'Add Role', align:'center', ellipsis: true, dataIndex: 'roleName', key: 'roleName' },
{ title: 'User Phone', align:'center', ellipsis: true, dataIndex: 'userPhone', key: 'userPhone' },
{ title: 'Owned Store', align:'center', ellipsis: true, dataIndex: 'storeName', key: 'storeName',customRender:(record:any)=>{
let storeNameNew = record.text.join(',')
return storeNameNew
}
},
{ title: 'Add Time', align:'center', ellipsis: true, dataIndex: 'createDate', key: 'createDate' ,customRender:(record:any)=>{
let time = formatTime(record.text / 1000, 'YYYY-MM-DD hh:mm:ss')
return time
},
},
{ title: 'Activated', align:'center', ellipsis: true, dataIndex: 'appUserName', key: 'appUserName' },
{ title: 'User State', align:'center', ellipsis: true, dataIndex: 'stateName', key: 'stateName' },
{
title: 'Operations',
key: 'operation',
align:'center',
fixed: 'right',
width: 150,
dataIndex:'operation',
},
])
let collectionList:any = ref([])
let currentPage = ref(1)
let pageSize = ref(10)
let total = ref(1)
let editUserModal = ref(false)
let editUserTitle = ref('Add user')
let formRef = ref();
let formState:any = ref({
id: '',
userName: '',
userAccount: '',
userPassword:'',
userPhone:'',
appUser:null,
roleId:null,
storeIds:[],
remarks:'',
});
let rules = {
userName: [
{ required: true, message: 'Please input user name', trigger: 'blur' },
],
userAccount: [
{ required: true, message: 'Please input user account', trigger: 'blur' },
{ min: 6, max: 20, message: 'Length should be 6 to 20', trigger: 'blur' },
],
userPassword: [
{ required: true, message: 'Please input user code', trigger: 'blur' },
{ min: 6, max: 20, message: 'Length should be 6 to 20', trigger: 'blur' },
],
userPhone: [
{ required: true, message: 'Please input user phone', trigger: 'blur' },
{ pattern: /^\d*$/, message: 'The format of the mobile phone number is incorrect', trigger: 'blur' },
],
appUser: [
{ required: true, message: 'Please select app user', trigger: 'blur' },
],
userRole: [
{ required: true, message: 'Please select user role', trigger: 'blur' },
],
ownerStore: [
{ required: true, message: 'Please select owner store', trigger: 'blur' },
],
}
let changePage = (e:any) =>{
currentPage.value = e.current
pageSize.value = e.pageSize
getUserlist()
}
let closeUser = () =>{
formState.value = {
id: '',
userName: '',
userAccount: '',
userPassword:'',
userPhone:'',
appUser:null,
roleId:null,
storeIds:[],
remarks:'',
}
editUserModal.value = false
}
let addUser = () =>{
editUserTitle.value = 'Add user'
editUserModal.value = true
}
let editUser = (data:any,) =>{
editUserModal.value = true
editUserTitle.value = 'Eidt user'
formState.value = {
id: data.id,
userName: data.userName,
userAccount:data.userAccount,
userPassword:data.userPassword ? decode(data.userPassword) : '',
userPhone:data.userPhone,
appUser:data.appUser,
roleId:String(data.roleId),
storeIds:data.storeIds,
remarks:data.remarks,
}
}
let getUserlist = () =>{
let data = {
userName : filter.userName,
userPhone:filter.userPhone,
state:filter.state && filter.state.length ? filter.state : '',
roleId:filter.roleId && filter.roleId.length ? filter.roleId : '',
createDateStart:filter.addTime && filter.addTime.length ? startTime(filter.addTime[0]) :'',
createDateEnd:filter.addTime && filter.addTime.length ? endTime(filter.addTime[1]) :'',
page:currentPage.value,
size:pageSize.value
}
tableLoading.value = true
Https.axiosPost(Https.httpUrls.accountQueryUserPage, data).then(
(rv: any) => {
if (rv) {
tableLoading.value = false
collectionList.value = rv.content
total.value = rv.total
}
}
);
}
let getRolelist = () =>{
Https.axiosPost(Https.httpUrls.roleQueryAll, {}).then(
(rv: any) => {
if (rv) {
userRoleList.value = rv.map((v:any)=>{
let data = {
...v,
value:v.id,
label:v.name
}
return data
})
}
}
);
}
let searchList = () =>{
currentPage.value = 1
getUserlist()
}
let resetList = () =>{
currentPage.value = 1
filter.userName = ''
filter.userPhone = ''
filter.roleId = []
filter.state = []
filter.addTime = ref<Moment[]>([])
getUserlist()
}
let confirmSubmit = () =>{
let data = {
...formState.value,
timeZone:Intl.DateTimeFormat().resolvedOptions().timeZone,
}
data.userPassword = encode(data.userPassword)
let submit = () =>{
Https.axiosPost(Https.httpUrls.accountSaveOrEdit, data).then(
(rv: any) => {
if (rv) {
let tip = !formState.value.id ? 'User added successfully' :'Edit user successfully'
message.success(tip)
resetList()
closeUser()
}
}
);
}
formRef.value.validate().then(()=>{
submit()
})
}
let enableUser = (record:any,index:any) =>{
let data ={
id:record.id,
enable:record.state == 1 ? 0 : 1
}
Https.axiosPost(Https.httpUrls.accountEnable, data).then(
(rv: any) => {
if (rv) {
let tip = record.state == 0? 'Enable successfully' :'Disable successfully'
message.success(tip)
collectionList.value[index].state = data.enable
collectionList.value[index].stateName = data.enable == 1 ? 'Activated' :'Deactivated'
}
}
);
}
let deleteUser = (data:any,index:any) =>{
let confirmDelete = (data:any,index:any) =>{
let newData = {
id:data.id
}
Https.axiosPost(Https.httpUrls.accountDelete, newData).then(
(rv: any) => {
if (rv) {
message.success('Delete success')
collectionList.value.splice(index,1)
}
}
);
}
Modal.confirm({
title: "Are you sure delete the user?",
icon: createVNode(WarningOutlined),
class:'confirm_style',
okText: 'Ok',
cancelText: 'Cancel',
// centered:true,
onOk() {
confirmDelete(data,index)
}
});
}
let storeQueryAll = () =>{
Https.axiosPost(Https.httpUrls.storeQueryAll,{}).then(
(rv: any) => {
if (rv) {
ownerStoreList.value = rv.map((v:any)=>{
let data = {
...v,
value:v.id,
label:v.name
}
return data
})
}
}
);
}
onMounted(() => {
if(route.query && route.query.type === 'add'){
addUser()
}
getUserlist()
storeQueryAll()
getRolelist()
})
return {
...toRefs(filter),
tableLoading,
userRoleList,
userStateList,
appUserList,
ownerStoreList,
columns,
collectionList,
currentPage,
pageSize,
total,
editUserModal,
editUserTitle,
formRef,
formState,
rules,
changePage,
closeUser,
addUser,
editUser,
searchList,
resetList,
confirmSubmit,
enableUser,
deleteUser
}
},
});
</script>
<style lang="less" scoped>
.user_manage_page{
padding-left: 28px;
.user_pic{
width: 50px;
}
.state_name_block{
display: flex;
align-items: center;
font-size: 14px;
justify-content: center;
&.enable_state{
color: #47E417;
.state_round{
background: #47E417;
}
}
&.disable_state{
color: #E41335;
.state_round{
background: #E41335;
}
}
.state_round{
width: 14px;
height: 14px;
border-radius: 50%;
margin-right: 5px;
}
}
.operate_list{
display: flex;
justify-content: space-between;
.operate_item{
font-size: 14px;
font-family: Roboto;
font-weight: 400;
color: #343579;
cursor: pointer;
&.operate_disable_state{
color: #E41335;
}
}
}
}
</style>

View File

@@ -0,0 +1,265 @@
<template>
<div class="worktable_page">
<div class="worktable_top_info">
<img class="worktable_logo" src="@/assets/images/worktable_logo.png">
<div class="worktable_top_left">
<div class="top_left_header">{{nowHours > 12 ? 'Afternoon':'Morning'}},{{userName}}.Have a nice day!</div>
<div class="work_user_info"><span class="user_info_margin">{{roleName}}</span><span class="user_info_margin">|</span><span>{{storeAddress}}{{storeName}}</span></div>
</div>
<div class="work_statistical_data_list">
<div class="work_statistical_data_item">
<div class="work_statistical_data_title">Store(s)</div>
<div class="work_statistical_data_num color_FF6D60">{{storeCount}}</div>
</div>
<div class="work_statistical_data_item">
<div class="work_statistical_data_title">Product(s)</div>
<div class="work_statistical_data_num color_0CB4B3">{{onSaleProductCount}}</div>
</div>
<div class="work_statistical_data_item">
<div class="work_statistical_data_title">Order(s)</div>
<div class="work_statistical_data_num color_FEAD75">{{orderSuccessCount}}</div>
</div>
</div>
</div>
<div class="worktable_content">
<div class="worktable_content_header">
<span class="icon iconfont icon-a-gengduocaidangongneng"></span>
<div>Quick access</div>
</div>
<div class="worktable_content_entry_list">
<div class="worktable_content_entry_item" @click="turnToPage({path:'/home/productmanage',type:''})" v-if="buttonShow.PRODUCT_LIST">
<img class="entry_bg" src="@/assets/images/green_bg.png">
<div class="entry_name">Product List</div>
</div>
<div class="worktable_content_entry_item" @click="turnToPage({path:'/home/productmanage',type:'add'})" v-if="buttonShow.PRODUCT_LIST">
<img class="entry_bg" src="@/assets/images/purple_bg.png">
<div class="entry_name">Add Product</div>
</div>
<div class="worktable_content_entry_item" @click="turnToPage({path:'/home/storemanage',type:''})" v-if="buttonShow.STORE_LIST">
<img class="entry_bg" src="@/assets/images/pink_bg.png">
<div class="entry_name">Store List</div>
</div>
<div class="worktable_content_entry_item" @click="turnToPage({path:'/home/storemanage',type:'add'})" v-if="buttonShow.STORE_LIST">
<img class="entry_bg" src="@/assets/images/orange_bg.png">
<div class="entry_name">Add Store</div>
</div>
<div class="worktable_content_entry_item" @click="turnToPage({path:'/home/usermanage',type:'add'})" v-if="buttonShow.USER_MANAGER">
<img class="entry_bg" src="@/assets/images/blue_bg.png">
<div class="entry_name">Add User</div>
</div>
</div>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent,reactive,ref,toRefs,onMounted } from "vue";
import { message } from "ant-design-vue";
import { useRouter,useRoute } from 'vue-router'
import { Https } from "@/tool/https";
export default defineComponent({
name:'worktable',
setup(){
const router = useRouter()
const workData = reactive({
userName:'',
roleName:'',
storeAddress:'',
storeName:'',
storeCount:0,
onSaleProductCount:0,
orderSuccessCount:0,
})
let userInfo:any = ref({})
let buttonShow:any = reactive({
PRODUCT_LIST:false,
STORE_LIST:false,
USER_MANAGER:false,
})
let nowHours = ref(0)
const turnToPage = (data:any) =>{
router.push({path:data.path,query:data.type?{type:data.type}:{}})
}
const getWorkData = () =>{
Https.axiosGet(Https.httpUrls.countWorkBench).then(
(rv: any) => {
if (rv) {
workData.userName = rv.userName
workData.roleName = rv.roleName
workData.storeAddress = rv.storeAddress
workData.storeName = rv.storeName
workData.storeCount = rv.storeCount
workData.onSaleProductCount = rv.onSaleProductCount
workData.orderSuccessCount = rv.orderSuccessCount
}
}
);
}
const getButtonRole = (menuList:any) =>{
let keyList = ['PRODUCT_LIST',"STORE_LIST","USER_MANAGER"]
for(let item of menuList){
if(!item.children){
if(keyList.includes(item.code)){
buttonShow[item.code] = item.isShow
}
}else{
getButtonRole(item.children)
}
}
}
onMounted(() => {
let menuList:any = sessionStorage.getItem('menuList')
menuList = JSON.parse(menuList)
let date = new Date()
nowHours.value = date.getHours()
getButtonRole(menuList)
getWorkData()
})
return {
turnToPage,
...toRefs(workData),
userInfo,
buttonShow,
nowHours,
}
},
});
</script>
<style lang="less" scoped>
.worktable_page{
padding-left: 28px;
.worktable_top_info{
background: #fff;
height: 120px;
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
padding-left: 117px;
position: relative;
margin-bottom: 30px;
.worktable_logo{
width: 100px;
height: 96px;
position: absolute;
left: 0;
bottom: 0;
}
.top_left_header{
font-size: 16px;
font-weight: normal;
color: #333333;
margin-bottom: 10px;
}
.work_user_info{
font-size: 16px;
color: #A0A1A6;
.user_info_margin{
margin-right: 10px;
}
}
.work_statistical_data_list{
display: flex;
align-items: center;
.work_statistical_data_item{
padding: 8px 40px 0;
height: 60px;
border-right:1px solid #E7E7E7;
:last-child{
border: none;
}
.work_statistical_data_title{
font-size: 16px;
line-height: 16px;
color: #333333;
margin-bottom: 12px;
}
.work_statistical_data_num{
font-size: 24px;
line-height: 24px;
text-align: center;
}
.color_FF6D60{
color: #FF6D60;
}
.color_0CB4B3{
color: #0CB4B3;
}
.color_FEAD75{
color: #FEAD75;
}
}
}
}
.worktable_content{
padding: 20px 25px 30px;
background: #fff;
.worktable_content_header{
display: flex;
align-items: center;
font-size: 16px;
color: #333333;
margin-bottom: 40px;
.icon-a-gengduocaidangongneng{
font-size: 24px;
color: #80B8F8;
margin-right: 14px;
}
}
.worktable_content_entry_list{
display: flex;
.worktable_content_entry_item{
position: relative;
width: 200px;
height: 114px;
padding-left: 20px;
display: flex;
align-items: center;
cursor: pointer;
margin-right: 20px;
.entry_bg{
position: absolute;
left: 0;
top: 0;
}
.entry_name{
color: #fff;
z-index: 2;
font-size: 19px;
}
}
}
}
}
</style>

44
tsconfig.json Normal file
View File

@@ -0,0 +1,44 @@
{
"compilerOptions": {
"target": "esnext",
"module": "esnext",
"strict": true,
"jsx": "preserve",
"moduleResolution": "node",
"experimentalDecorators": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"useDefineForClassFields": true,
"sourceMap": true,
"allowJs": true, //编译时允许有js
"baseUrl": ".",
"outDir": "./",
"types": [
"webpack-env"
],
"paths": {
"@/*": [
"src/*"
]
},
"lib": [
"esnext",
"dom",
"dom.iterable",
"scripthost"
]
},
"include": [
"src/**/*.ts",
"src/**/*.tsx",
"src/**/*.vue",
"tests/**/*.ts",
"tests/**/*.tsx",
"src/**/*.js",
],
"exclude": [
"node_modules"
]
}

45
vue.config.js Normal file
View File

@@ -0,0 +1,45 @@
const {defineConfig} = require('@vue/cli-service')
const path = require('path');
module.exports = defineConfig({
transpileDependencies: ['vuetify'],
lintOnSave:false,//关闭语法检查
devServer: {
// hot: true, // 热更新
port: '8060',
proxy: {
"/api": {
// target: 'http://18.167.251.121:5568', //后端接口地址
target: 'http://192.168.1.9:5560/', //后端接口地址
changeOrigin: true, //是否允许跨越
}
},
},
pluginOptions: {
"style-resources-loader": {
preProcessor: "less",
patterns: [
// 存放less变量文件的路径
path.resolve(__dirname, "./src/assets/style/style.less")
]
}
},
css: {
loaderOptions: {
less: {
lessOptions: {
modifyVars: {
'primary-color': '#ec6800'
},
javascriptEnabled: true,
},
},
},
}
})