master
fy 2026-01-19 16:52:24 +08:00
commit ce5b350705
417 changed files with 56226 additions and 0 deletions

16
.gitignore vendored Normal file
View File

@ -0,0 +1,16 @@
######################################################################
# Build Tools
/unpackage/*
/node_modules/*
######################################################################
# Development Tools
/.idea/*
/.vscode/*
/.hbuilderx/*
package-lock.json
yarn.lock

View File

@ -0,0 +1,21 @@
## 修改浅色模式样式,增强模块区分度
### 修改内容:
1. **调整CSS变量**(在 `:root` 中)
- 背景色:`#f5f7fa` → `#e8ecf1`(增强对比)
- 卡片阴影:`rgba(0, 0, 0, 0.05)` → `rgba(0, 0, 0, 0.12)`(更明显)
- 边框色:`#e0e0e0` → `#d1d5db`(更清晰的边框)
- 分割线:`darkgray` → `#d1d5db`
2. **增强卡片样式**
- 添加微妙的边框效果
- 增强阴影深度
- 优化卡片与背景的对比
3. **保持深色模式不变**
- 深色模式已经很清晰,无需修改
### 预期效果:
- 模块之间区分更明显
- 视觉层次更清晰
- 保持简洁风格的同时提升可读性

33
App.vue Normal file
View File

@ -0,0 +1,33 @@
<script>
import config from './config'
import { getToken } from '@/utils/auth'
export default {
onLaunch: function() {
this.initApp()
},
methods: {
//
initApp() {
//
this.initConfig()
//
//#ifdef H5
this.checkLogin()
//#endif
},
initConfig() {
this.globalData.config = config
},
checkLogin() {
if (!getToken()) {
this.$tab.reLaunch('/pages/login')
}
}
}
}
</script>
<style lang="scss">
@import '@/static/scss/index.scss'
</style>

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 若依
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

51
README.md Normal file
View File

@ -0,0 +1,51 @@
<p align="center">
<img alt="logo" src="https://oscimg.oschina.net/oscnet/up-43e3941654fa3054c9684bf53d1b1d356a1.png">
</p>
<h1 align="center" style="margin: 30px 0 30px; font-weight: bold;">RuoYi v1.2.0</h1>
<h4 align="center">基于UniApp开发的轻量级移动端框架</h4>
<p align="center">
<a href="https://gitee.com/y_project/RuoYi-App/stargazers"><img src="https://gitee.com/y_project/RuoYi-App/badge/star.svg?theme=dark"></a>
<a href="https://gitee.com/y_project/RuoYi-App"><img src="https://img.shields.io/badge/RuoYi-v1.2.0-brightgreen.svg"></a>
<a href="https://gitee.com/y_project/RuoYi-App/blob/master/LICENSE"><img src="https://img.shields.io/github/license/mashape/apistatus.svg"></a>
</p>
## 平台简介
RuoYi App 移动解决方案采用uniapp框架一份代码多终端适配同时支持APP、小程序、H5实现了与[RuoYi-Vue](https://gitee.com/y_project/RuoYi-Vue)、[RuoYi-Cloud](https://gitee.com/y_project/RuoYi-Cloud)完美对接的移动解决方案!目前已经实现登录、我的、工作台、编辑资料、头像修改、密码修改、常见问题、关于我们等基础功能。
* 配套后端代码仓库地址[RuoYi-Vue](https://gitee.com/y_project/RuoYi-Vue) 或 [RuoYi-Cloud](https://github.com/yangzongzhuan/RuoYi-Cloud) 版本。
* 应用框架基于[uniapp](https://uniapp.dcloud.net.cn/)支持小程序、H5、Android和IOS。
* 前端组件采用[uni-ui](https://github.com/dcloudio/uni-ui)全端兼容的高性能UI框架。
* 阿里云折扣场:[点我进入](http://aly.ruoyi.vip),腾讯云秒杀场:[点我进入](http://txy.ruoyi.vip)&nbsp;&nbsp;
## 技术文档
- 官网网站:[http://ruoyi.vip](http://ruoyi.vip)
- 文档地址:[http://doc.ruoyi.vip](http://doc.ruoyi.vip)
- H5页体验[http://h5.ruoyi.vip](http://h5.ruoyi.vip)
- QQ交流群 ①133713780(满)、②146013835(满)、③189091635
- 小程序体验
<img src="https://oscimg.oschina.net/oscnet/up-26c76dc90b92acdbd9ac8cd5252f07c8ad9.jpg" alt="小程序演示"/>
## 演示图
<table>
<tr>
<td><img src="https://oscimg.oschina.net/oscnet/up-21f6f842fdc94540469b4eb43fdadbaf7f8.png"/></td>
<td><img src="https://oscimg.oschina.net/oscnet/up-a6f23cf9a371a30165e135eff6d9ae89a9d.png"/></td>
<td><img src="https://oscimg.oschina.net/oscnet/up-ff5f62016bf6624c1ff27eee57499dccd44.png"/></td>
</tr>
<tr>
<td><img src="https://oscimg.oschina.net/oscnet/up-b9a582fdb26ec69d407fabd044d2c8494df.png"/></td>
<td><img src="https://oscimg.oschina.net/oscnet/up-96427ee08fca29d77934cfc8d1b1a637cef.png"/></td>
<td><img src="https://oscimg.oschina.net/oscnet/up-5fdadc582d24cccd7727030d397b63185a3.png"/></td>
</tr>
<tr>
<td><img src="https://oscimg.oschina.net/oscnet/up-0a36797b6bcc50c36d40c3c782665b89efc.png"/></td>
<td><img src="https://oscimg.oschina.net/oscnet/up-d77995cc00687cedd00d5ac7d68a07ea276.png"/></td>
<td><img src="https://oscimg.oschina.net/oscnet/up-fa8f5ab20becf59b4b38c1b92a9989e7109.png"/></td>
</tr>
</table>

60
api/login.js Normal file
View File

@ -0,0 +1,60 @@
import request from '@/utils/request'
// 登录方法
export function login(username, password, code, uuid) {
const data = {
username,
password,
code,
uuid
}
return request({
'url': '/login',
headers: {
isToken: false
},
'method': 'post',
'data': data
})
}
// 注册方法
export function register(data) {
return request({
url: '/register',
headers: {
isToken: false
},
method: 'post',
data: data
})
}
// 获取用户详细信息
export function getInfo() {
return request({
'url': '/getInfo',
'method': 'get'
})
}
// 退出方法
export function logout() {
return request({
'url': '/logout',
'method': 'post'
})
}
// 获取验证码
export function getCodeImg() {
return request({
'url': '/captchaImage',
headers: {
isToken: false
},
method: 'get',
timeout: 20000
})
}

256
api/product.js Normal file
View File

@ -0,0 +1,256 @@
import { getToken } from '@/utils/auth'
import { toast } from '@/utils/common'
// 参数转URL查询字符串
function tansParams(params) {
let result = ''
for (const propName of Object.keys(params)) {
const value = params[propName]
var part = encodeURIComponent(propName) + "="
if (value !== null && value !== "" && typeof (value) !== "undefined") {
if (typeof value === 'object') {
for (const key of Object.keys(value)) {
if (value[key] !== null && value[key] !== "" && typeof (value[key]) !== 'undefined') {
let params = propName + '[' + key + ']'
var subPart = encodeURIComponent(params) + "="
result += subPart + encodeURIComponent(value[key]) + "&"
}
}
} else {
result += part + encodeURIComponent(value) + "&"
}
}
}
return result
}
// 专门用于8081端口的请求
const request8081 = config => {
config.header = config.header || {}
// 8081端口可能使用不同的认证方式
const token = getToken()
console.log('=== 8081端口请求详情 ===')
console.log('Token值:', token)
console.log('Token存在:', !!token)
if (token) {
// 尝试多种认证方式
// 方式1Bearer Token标准JWT
config.header['Authorization'] = 'Bearer ' + token
// 方式2直接使用token不带Bearer
// config.header['Authorization'] = token
// 方式3自定义认证头
// config.header['X-Auth-Token'] = token
// 方式4Cookie方式
// config.header['Cookie'] = 'token=' + token
}
// 处理GET请求参数
let requestUrl = config.baseUrl + config.url
if (config.params) {
let url = requestUrl + '?' + tansParams(config.params)
requestUrl = url.slice(0, -1)
}
console.log('使用的认证方式:', 'Bearer Token')
console.log('请求头:', config.header)
console.log('请求URL:', requestUrl)
console.log('请求参数:', config.params)
console.log('请求方法:', config.method)
return new Promise((resolve, reject) => {
// 为PUT请求设置Content-Type
if (config.method && config.method.toLowerCase() === 'put') {
config.header = {
...config.header,
'Content-Type': 'application/json'
}
}
uni.request({
method: config.method || 'get',
timeout: config.timeout || 10000,
url: requestUrl,
data: config.data,
header: config.header,
dataType: 'json'
}).then(response => {
let [error, res] = response
console.log('=== 8081端口响应详情 ===')
// console.log('响应状态码:', res.statusCode)
console.log('响应头:', res.header)
console.log('响应数据:', res.data)
console.log('响应数据类型:', typeof res.data)
if (error) {
toast('后端接口连接异常')
reject('后端接口连接异常')
return
}
const code = res.data.code || 200
const msg = res.data.msg || '请求失败'
// console.log('业务状态码:', code)
// console.log('业务消息:', msg)
if (code === 401) {
toast('认证失败,请重新登录')
reject('401')
} else if (code === 403) {
// console.log('=== 403错误详细信息 ===')
// console.log('完整响应:', JSON.stringify(res, null, 2))
toast('没有权限访问该资源')
reject('403')
} else if (code === 500) {
toast(msg)
reject('500')
} else if (code !== 200) {
toast(msg)
reject(code)
}
resolve(res.data)
}).catch(error => {
console.error('8081端口请求异常:', error)
toast('网络请求失败')
reject(error)
})
})
}
// 获取商品列表
export function getProductList(params) {
const token = getToken()
// console.log('=== 商品列表请求诊断 ===')
// console.log('1. Token值:', token)
// console.log('2. Token长度:', token ? token.length : 0)
// console.log('3. Token前缀:', token ? token.substring(0, 20) + '...' : '无')
// console.log('4. 请求URL:', 'http://193.112.94.36:8081/mall/product/list')
// console.log('5. 认证方式:', 'Bearer Token (与8080共享)')
return request8081({
baseUrl: 'http://193.112.94.36:8081',
url: '/mall/product/list',
method: 'get',
params: params
})
}
// 删除商品
export function deleteProduct(id) {
return request8081({
baseUrl: 'http://193.112.94.36:8081',
url: `/mall/product/delete/${id}`,
method: 'delete'
})
}
// 新增商品
export function addProduct(data) {
return request8081({
baseUrl: 'http://193.112.94.36:8081',
url: '/mall/product/add',
method: 'post',
data: data
})
}
// 新增商品(带文件上传)
export function addProductWithFile(filePath, formData) {
return new Promise((resolve, reject) => {
uni.uploadFile({
url: 'http://193.112.94.36:8081/mall/product/add',
filePath: filePath,
name: 'file',
formData: formData,
header: {
'Authorization': 'Bearer ' + getToken()
},
success: (uploadRes) => {
const res = JSON.parse(uploadRes.data);
resolve(res);
},
fail: (error) => {
reject(error);
}
});
});
}
// 获取商品详情
export function getProductDetail(id) {
return request8081({
baseUrl: 'http://193.112.94.36:8081',
url: `/mall/product/${id}`,
method: 'get'
})
}
// 修改商品form-data格式支持文件上传
export function updateProductWithFile(filePath, formData) {
return new Promise((resolve, reject) => {
uni.uploadFile({
url: 'http://193.112.94.36:8081/mall/product/update',
filePath: filePath,
name: 'file',
formData: formData,
header: {
'Authorization': 'Bearer ' + getToken()
},
success: (uploadRes) => {
const res = JSON.parse(uploadRes.data);
resolve(res);
},
fail: (error) => {
reject(error);
}
});
});
}
// 修改商品
export function updateProduct(data) {
return request8081({
baseUrl: 'http://193.112.94.36:8081',
url: '/mall/product/update',
method: 'post',
data: data
})
}
// 商品数据导入(文件上传)
export function importProductData(filePath, formData) {
return new Promise((resolve, reject) => {
uni.uploadFile({
url: 'http://193.112.94.36:8081/mall/product/importData',
filePath: filePath,
name: 'file',
formData: formData,
header: {
'Authorization': 'Bearer ' + getToken()
},
success: (uploadRes) => {
const res = JSON.parse(uploadRes.data);
resolve(res);
},
fail: (error) => {
reject(error);
}
});
});
}
// 获取导入记录
export function getImportRecord() {
return request8081({
baseUrl: 'http://192.168.0.7:8081',
url: '/mall/product/importRecord',
method: 'get'
});
}

11
api/store.js Normal file
View File

@ -0,0 +1,11 @@
import request from '@/utils/request'
// 根据用户ID查询门店列表
export function getStoreList(userId) {
return request({
baseUrl: 'http://193.112.94.36:8081',
url: `/mall/store/getUserStore/${userId}`,
method: 'get'
})
}

60
api/system/dict/data.js Normal file
View File

@ -0,0 +1,60 @@
import request from '@/utils/request'
// 查询字典数据列表
export function listData(query) {
return request({
url: '/system/dict/data/list',
method: 'get',
params: query
})
}
// 查询字典数据详细
export function getData(dictCode) {
return request({
url: '/system/dict/data/' + dictCode,
method: 'get'
})
}
// 根据字典类型查询字典数据信息
export function getDicts(dictType) {
return request({
url: '/system/dict/data/type/' + dictType,
method: 'get'
})
}
// 新增字典数据
export function addData(data) {
return request({
url: '/system/dict/data',
method: 'post',
data: data
})
}
// 修改字典数据
export function updateData(data) {
return request({
url: '/system/dict/data',
method: 'put',
data: data
})
}
// 删除字典数据
export function delData(dictCode) {
return request({
url: '/system/dict/data/' + dictCode,
method: 'delete'
})
}
// 测试接口
export function getOen() {
return request({
url: '/system/config/test',
method: 'get'
})
}

60
api/system/dict/type.js Normal file
View File

@ -0,0 +1,60 @@
import request from '@/utils/request'
// 查询字典类型列表
export function listType(query) {
return request({
url: '/system/dict/type/list',
method: 'get',
params: query
})
}
// 查询字典类型详细
export function getType(dictId) {
return request({
url: '/system/dict/type/' + dictId,
method: 'get'
})
}
// 新增字典类型
export function addType(data) {
return request({
url: '/system/dict/type',
method: 'post',
data: data
})
}
// 修改字典类型
export function updateType(data) {
return request({
url: '/system/dict/type',
method: 'put',
data: data
})
}
// 删除字典类型
export function delType(dictId) {
return request({
url: '/system/dict/type/' + dictId,
method: 'delete'
})
}
// 刷新字典缓存
export function refreshCache() {
return request({
url: '/system/dict/type/refreshCache',
method: 'delete'
})
}
// 获取字典选择框列表
export function optionselect() {
return request({
url: '/system/dict/type/optionselect',
method: 'get'
})
}

41
api/system/user.js Normal file
View File

@ -0,0 +1,41 @@
import upload from '@/utils/upload'
import request from '@/utils/request'
// 用户密码重置
export function updateUserPwd(oldPassword, newPassword) {
const data = {
oldPassword,
newPassword
}
return request({
url: '/system/user/profile/updatePwd',
method: 'put',
data: data
})
}
// 查询用户个人信息
export function getUserProfile() {
return request({
url: '/system/user/profile',
method: 'get'
})
}
// 修改用户个人信息
export function updateUserProfile(data) {
return request({
url: '/system/user/profile',
method: 'put',
data: data
})
}
// 用户头像上传
export function uploadAvatar(data) {
return upload({
url: '/system/user/profile/avatar',
name: data.name,
filePath: data.filePath
})
}

206
components/ScanView.vue Normal file
View File

@ -0,0 +1,206 @@
<template>
<view class="scan-view-container">
<!-- 扫码控件容器 -->
<view class="scan-box" id="scan-box"></view>
<!-- 扫码状态 -->
<view class="scan-status" v-if="isScanning">
<view class="scan-line"></view>
<text class="scan-text">正在扫描...</text>
</view>
<!-- 扫码控制按钮 -->
<view class="scan-controls" v-if="isScanning">
<view class="control-btn" @click="toggleFlash">
<uni-icons type="flash" size="22" color="#fff"></uni-icons>
<text class="control-text">{{ flashOn ? '关闭手电' : '开启手电' }}</text>
</view>
<view class="control-btn" @click="pauseScan">
<uni-icons type="pause" size="22" color="#fff"></uni-icons>
<text class="control-text">{{ scanPaused ? '继续扫码' : '暂停扫码' }}</text>
</view>
</view>
</view>
</template>
<script>
export default {
name: 'ScanView',
props: {
autoStart: {
type: Boolean,
default: false
}
},
data() {
return {
isScanning: false,
scanPaused: false,
flashOn: false,
barcodeInstance: null
};
},
mounted() {
if (this.autoStart) {
this.initScan();
}
},
beforeUnmount() {
this.closeScan();
},
methods: {
initScan() {
if (typeof plus === 'undefined') {
console.log('当前环境不支持plus扫码');
this.$emit('error', '扫码功能需要在App中使用');
return;
}
console.log('开始初始化扫码控件');
this.isScanning = true;
const barcode = plus.barcode.create('barcode', [plus.barcode.CODE_128, plus.barcode.EAN_13], {
top: '0',
left: '0',
width: '100%',
height: '100%',
position: 'absolute'
});
console.log('扫码控件创建成功');
barcode.onmarked = (type, result) => {
console.log('识别成功:', result);
this.isScanning = false;
this.$emit('success', result);
};
barcode.onerror = (error) => {
console.error('扫码错误:', error);
this.isScanning = false;
this.$emit('error', error || '扫码失败');
};
const scanBox = document.getElementById('scan-box');
if (scanBox) {
scanBox.appendChild(barcode);
}
barcode.start();
console.log('扫码已启动');
this.barcodeInstance = barcode;
},
closeScan() {
if (this.barcodeInstance) {
this.barcodeInstance.close();
this.barcodeInstance = null;
this.isScanning = false;
console.log('扫码控件已关闭');
}
},
pauseScan() {
this.scanPaused = !this.scanPaused;
if (this.barcodeInstance) {
this.scanPaused ? this.barcodeInstance.pause() : this.barcodeInstance.resume();
}
this.$emit('pause', this.scanPaused);
},
resumeScan() {
if (this.barcodeInstance && this.scanPaused) {
this.barcodeInstance.resume();
this.scanPaused = false;
this.isScanning = true;
}
},
toggleFlash() {
this.flashOn = !this.flashOn;
if (this.barcodeInstance) {
this.barcodeInstance.setFlash(this.flashOn);
}
this.$emit('flash', this.flashOn);
}
}
};
</script>
<style scoped>
.scan-view-container {
width: 100%;
height: 100%;
position: relative;
background-color: #000;
}
.scan-box {
width: 100%;
height: 100%;
position: relative;
}
.scan-status {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background-color: rgba(0, 0, 0, 0.3);
}
.scan-line {
width: 80%;
height: 2px;
background-color: #e60012;
animation: scanMove 2s infinite;
}
@keyframes scanMove {
0% {
transform: translateY(0);
opacity: 0;
}
50% {
opacity: 1;
}
100% {
transform: translateY(250px);
opacity: 0;
}
}
.scan-text {
color: #fff;
font-size: 16px;
margin-top: 20px;
}
.scan-controls {
position: absolute;
bottom: 30px;
left: 0;
width: 100%;
display: flex;
justify-content: space-around;
padding: 0 20px;
}
.control-btn {
display: flex;
flex-direction: column;
align-items: center;
color: #fff;
cursor: pointer;
}
.control-text {
font-size: 12px;
margin-top: 5px;
}
</style>

View File

@ -0,0 +1,167 @@
<template>
<view class="uni-section">
<view class="uni-section-header" @click="onClick">
<view class="uni-section-header__decoration" v-if="type" :class="type" />
<slot v-else name="decoration"></slot>
<view class="uni-section-header__content">
<text :style="{'font-size':titleFontSize,'color':titleColor}" class="uni-section__content-title" :class="{'distraction':!subTitle}">{{ title }}</text>
<text v-if="subTitle" :style="{'font-size':subTitleFontSize,'color':subTitleColor}" class="uni-section-header__content-sub">{{ subTitle }}</text>
</view>
<view class="uni-section-header__slot-right">
<slot name="right"></slot>
</view>
</view>
<view class="uni-section-content" :style="{padding: _padding}">
<slot />
</view>
</view>
</template>
<script>
/**
* Section 标题栏
* @description 标题栏
* @property {String} type = [line|circle|square] 标题装饰类型
* @value line 竖线
* @value circle 圆形
* @value square 正方形
* @property {String} title 主标题
* @property {String} titleFontSize 主标题字体大小
* @property {String} titleColor 主标题字体颜色
* @property {String} subTitle 副标题
* @property {String} subTitleFontSize 副标题字体大小
* @property {String} subTitleColor 副标题字体颜色
* @property {String} padding 默认插槽 padding
*/
export default {
name: 'UniSection',
emits:['click'],
props: {
type: {
type: String,
default: ''
},
title: {
type: String,
required: true,
default: ''
},
titleFontSize: {
type: String,
default: '14px'
},
titleColor:{
type: String,
default: '#333'
},
subTitle: {
type: String,
default: ''
},
subTitleFontSize: {
type: String,
default: '12px'
},
subTitleColor: {
type: String,
default: '#999'
},
padding: {
type: [Boolean, String],
default: false
}
},
computed:{
_padding(){
if(typeof this.padding === 'string'){
return this.padding
}
return this.padding?'10px':''
}
},
watch: {
title(newVal) {
if (uni.report && newVal !== '') {
uni.report('title', newVal)
}
}
},
methods: {
onClick() {
this.$emit('click')
}
}
}
</script>
<style lang="scss" >
$uni-primary: #2979ff !default;
.uni-section {
background-color: #fff;
.uni-section-header {
position: relative;
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: row;
align-items: center;
padding: 12px 10px;
font-weight: normal;
&__decoration{
margin-right: 6px;
background-color: $uni-primary;
&.line {
width: 4px;
height: 12px;
border-radius: 10px;
}
&.circle {
width: 8px;
height: 8px;
border-top-right-radius: 50px;
border-top-left-radius: 50px;
border-bottom-left-radius: 50px;
border-bottom-right-radius: 50px;
}
&.square {
width: 8px;
height: 8px;
}
}
&__content {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: column;
flex: 1;
color: #333;
.distraction {
flex-direction: row;
align-items: center;
}
&-sub {
margin-top: 2px;
}
}
&__slot-right{
font-size: 14px;
}
}
.uni-section-content{
font-size: 14px;
}
}
</style>

29
config.js Normal file
View File

@ -0,0 +1,29 @@
// 应用全局配置
module.exports = {
// baseUrl: 'https://vue.ruoyi.vip/prod-api',
baseUrl:'http://193.112.94.36:8080',
// prodApi: 'https://vue.ruoyi.vip/prod-api',
// baseUrl: 'http://localhost:8080',
// 应用信息
appInfo: {
// 应用名称
name: "ruoyi-app",
// 应用版本
version: "1.2.0",
// 应用logo
logo: "/static/logo.png",
// 官方网站
site_url: "http://ruoyi.vip",
// 政策协议
agreements: [{
title: "隐私政策",
url: "https://ruoyi.vip/protocol.html"
},
{
title: "用户服务协议",
url: "https://ruoyi.vip/protocol.html"
}
]
}
}

20
main.js Normal file
View File

@ -0,0 +1,20 @@
import Vue from 'vue'
import App from './App'
import store from './store' // store
import plugins from './plugins' // plugins
import './permission' // permission
import { getDicts } from "@/api/system/dict/data"
Vue.use(plugins)
Vue.config.productionTip = false
Vue.prototype.$store = store
Vue.prototype.getDicts = getDicts
App.mpType = 'app'
const app = new Vue({
...App
})
app.$mount()

78
manifest.json Normal file
View File

@ -0,0 +1,78 @@
{
"name" : "若依移动端",
"appid" : "__UNI__34800B4",
"description" : "111",
"versionName" : "1.2.0",
"versionCode" : "100",
"transformPx" : false,
"app-plus" : {
"usingComponents" : true,
"nvueCompiler" : "uni-app",
"splashscreen" : {
"alwaysShowBeforeRender" : true,
"waiting" : true,
"autoclose" : true,
"delay" : 0
},
"modules" : {
"Barcode" : {},
"Camera" : {}
},
"distribute" : {
"android" : {
"permissions" : [
"<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
"<uses-permission android:name=\"android.permission.VIBRATE\"/>",
"<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
"<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.CAMERA\"/>",
"<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
"<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
"<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
"<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
"<uses-feature android:name=\"android.hardware.camera\"/>",
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
]
},
"ios" : {
"dSYMs" : false,
"privacyDescription" : {
"NSCameraUsageDescription" : "需要使用相机扫描条码",
"NSPhotoLibraryUsageDescription" : "需要访问相册选择图片扫码"
}
},
"sdkConfigs" : {}
}
},
"quickapp" : {},
"mp-weixin" : {
"appid" : "wxccd7e2a0911b3397",
"setting" : {
"urlCheck" : false,
"es6" : false,
"minified" : true,
"postcss" : true
},
"optimization" : {
"subPackages" : true
},
"usingComponents" : true
},
"vueVersion" : "2",
"h5" : {
"template" : "static/index.html",
"devServer" : {
"port" : 9090,
"https" : false
},
"title" : "RuoYi-App",
"router" : {
"mode" : "hash",
"base" : "./"
}
}
}

176
pages.json Normal file
View File

@ -0,0 +1,176 @@
{
"pages": [{
"path": "pages/login",
"style": {
"navigationBarTitleText": "登录"
}
},
{
"path": "pages/register",
"style": {
"navigationBarTitleText": "注册"
}
}, {
"path": "pages/index",
"style": {
"navigationBarTitleText": "若依移动端框架",
"navigationStyle": "custom"
}
}, {
"path": "pages/work/index",
"style": {
"navigationBarTitleText": "工作台"
}
}, {
"path": "pages/mine/index",
"style": {
"navigationBarTitleText": "我的"
}
}, {
"path": "pages/mine/avatar/index",
"style": {
"navigationBarTitleText": "修改头像"
}
}, {
"path": "pages/mine/info/index",
"style": {
"navigationBarTitleText": "个人信息"
}
}, {
"path": "pages/mine/info/edit",
"style": {
"navigationBarTitleText": "编辑资料"
}
}, {
"path": "pages/mine/pwd/index",
"style": {
"navigationBarTitleText": "修改密码"
}
}, {
"path": "pages/mine/setting/index",
"style": {
"navigationBarTitleText": "应用设置"
}
}, {
"path": "pages/mine/help/index",
"style": {
"navigationBarTitleText": "常见问题"
}
}, {
"path": "pages/mine/about/index",
"style": {
"navigationBarTitleText": "关于我们"
}
}, {
"path": "pages/common/webview/index",
"style": {
"navigationBarTitleText": "浏览网页"
}
}, {
"path": "pages/common/textview/index",
"style": {
"navigationBarTitleText": "浏览文本"
}
},
{
"path": "pages/menu",
"style": {
"navigationBarTitleText": ""
}
},
{
"path": "pages/user/user",
"style": {
"navigationBarTitleText": ""
}
},
{
"path": "pages/settings/settings",
"style": {
"navigationBarTitleText": ""
}
},
{
"path": "pages/product/product",
"style": {
"navigationBarTitleText": ""
}
},
{
"path": "pages/asset/asset",
"style": {
"navigationBarTitleText": ""
}
},
{
"path": "pages/enter/enter",
"style": {
"navigationBarTitleText": ""
}
},
{
"path": "pages/addProduct/addProduct",
"style": {
"navigationBarTitleText": ""
}
},
{
"path": "pages/storeSelect/storeSelect",
"style": {
"navigationBarTitleText": "选择门店"
}
},
{
"path": "pages/edit/edit",
"style": {
"navigationBarTitleText": ""
}
},
{
"path": "pages/Import /Import ",
"style": {
"navigationBarTitleText": ""
}
},
{
"path": "pages/userStores/userStores",
"style": {
"navigationBarTitleText": "用户门店关联"
}
},
{
"path": "pages/userStores/userStores",
"style": {
"navigationBarTitleText": ""
}
}
],
"tabBar": {
"color": "#000000",
"selectedColor": "#000000",
"borderStyle": "white",
"backgroundColor": "#ffffff",
"list": [{
"pagePath": "pages/index",
"iconPath": "/static/images/tabbar/Frame 87.png",
"selectedIconPath": "/static/images/tabbar/Frame 86.png",
"text": "我的店"
}, {
"pagePath": "pages/work/index",
"iconPath": "/static/images/tabbar/Union.png",
"selectedIconPath": "/static/images/tabbar/Union-1.png",
"text": "热销榜"
}, {
"pagePath": "pages/mine/index",
"iconPath": "/static/images/tabbar/Vector-1.png",
"selectedIconPath": "/static/images/tabbar/Vector.png",
"text": "消息"
}
]
},
"globalStyle": {
"navigationBarTextStyle": "black",
"navigationBarTitleText": "RuoYi",
"navigationBarBackgroundColor": "#FFFFFF"
}
}

493
pages/Import /Import .vue Normal file
View File

@ -0,0 +1,493 @@
<template>
<view class="container">
<!-- 顶部导航栏 -->
<view class="navbar">
<view class="nav-left" @click="goBack">
<text class="iconfont"></text>
</view>
<view class="nav-title">批量导入</view>
<view class="nav-right">
<text class="iconfont">···</text>
</view>
</view>
<!-- 选项卡切换 -->
<view class="tab-bar">
<view class="tab-item" :class="{active: activeTab === 'import'}" @click="activeTab = 'import'">
批量导入
</view>
<view class="tab-item" :class="{active: activeTab === 'record'}" @click="activeTab = 'record'">
导入记录
</view>
</view>
<!-- 批量导入内容区 -->
<view v-if="activeTab === 'import'" class="content">
<!-- 方式1导入进货单核心带完整上传功能 -->
<view class="card">
<view class="card-header">
<view class="method-tag">方式1</view>
<view class="method-title">导入进货单</view>
</view>
<view class="upload-area" @click="pickFile" v-if="!uploadFileName">
<view class="upload-icon">
<text class="iconfont"></text>
</view>
<view class="upload-text">点击上传文件</view>
<view class="upload-desc">仅支持10M以下以xlsxlsx或csv结尾的文件类型</view>
</view>
<!-- 已选择文件展示 -->
<view class="file-selected" v-else>
<view class="file-name">{{uploadFileName}}</view>
<view class="file-opt">
<text class="reupload" @click="pickFile"></text>
<text class="cancel" @click="cancelFile"></text>
</view>
</view>
<!-- 导入按钮 -->
<view class="import-btn-box" v-if="uploadFileName">
<button class="import-btn" :loading="isUploading" @click="uploadAndImport"></button>
</view>
<view class="desc-list">
<text class="desc-item">1. 可导入供应商进货单其他收银系统的商品导出明细</text>
<text class="desc-item">2. 支持录入条形码商品名称零售价库存进货价商品品牌</text>
<text class="desc-item">3. 导入成功后会在原商品基础上增加库存不会覆盖原库存</text>
</view>
</view>
<!-- 方式2通过电脑端录入 -->
<view class="card">
<view class="card-header">
<view class="method-tag">方式2</view>
<view class="method-title">通过电脑端录入</view>
</view>
<view class="card-content">
<view class="content-text">支持导入更大文件更多商品数商品信息</view>
<view class="link-area" @click="openLink">
<text class="link-text">请在电脑端访问 http://e.weidian.com/main</text>
<text class="iconfont"></text>
</view>
</view>
</view>
<!-- 方式3微店商品快速录入 -->
<view class="card">
<view class="card-header">
<view class="method-tag">方式3</view>
<view class="method-title">微店商品快速录入</view>
</view>
<view class="card-content">
<view class="content-text">如果您没有电脑可以试试我们的快捷设置功能</view>
<view class="link-area" @click="goQuickSetup">
<text class="link-text">3-20分钟快速设置价格 ></text>
</view>
</view>
</view>
</view>
<!-- 导入记录内容区补充列表骨架+空数据 -->
<view v-else class="record-content">
<view class="empty-state" v-if="recordList.length == 0">
<text class="empty-icon">📋</text>
<text class="empty-text">暂无导入记录</text>
<text class="empty-subtext">导入商品后记录将展示在这里</text>
</view>
<view class="record-list" v-else>
<view class="record-item" v-for="(item,index) in recordList" :key="index">
<view class="record-name">{{item.fileName}}</view>
<view class="record-info">
<text class="record-time">{{item.createTime}}</text>
<text class="record-status" :class="item.status">{{item.statusText}}</text>
</view>
</view>
</view>
</view>
</view>
</template>
<script>
// API
import { importProductData, getImportRecord } from '@/api/product';
// ID
import { getStoreId } from '@/utils/auth';
export default {
data() {
return {
activeTab: 'import', //
uploadFilePath: '', //
uploadFileName: '', //
isUploading: false, // /
recordList: [] //
}
},
onShow() {
//
if(this.activeTab == 'record'){
this.getImportRecord();
}
},
methods: {
//
goBack() {
uni.navigateBack({ delta: 1 });
},
// ========== ==========
pickFile() {
uni.chooseFile({
count: 1, // 1
type: 'file',
sizeLimit: 10 * 1024 * 1024, // 10M
success: (res) => {
const tempFile = res.tempFiles[0];
const fileName = tempFile.name;
const fileSize = tempFile.size;
// 1.
if (fileSize > 10 * 1024 * 1024) {
return uni.showToast({ title: '文件大小不能超过10M', icon: 'none', duration: 2000 });
}
// 2. (xls/xlsx/csv)
const suffix = fileName.substring(fileName.lastIndexOf('.')).toLowerCase();
const allowSuffix = ['.xls', '.xlsx', '.csv'];
if (!allowSuffix.includes(suffix)) {
return uni.showToast({ title: '仅支持xls、xlsx、csv格式文件', icon: 'none', duration: 2000 });
}
//
this.uploadFilePath = tempFile.path;
this.uploadFileName = fileName;
uni.showToast({ title: '文件选择成功', icon: 'success', duration: 1500 });
},
fail: () => {
uni.showToast({ title: '文件选择失败,请重新选择', icon: 'none' });
}
});
},
//
cancelFile() {
this.uploadFilePath = '';
this.uploadFileName = '';
},
// ========== + ==========
uploadAndImport() {
if (!this.uploadFilePath) return;
this.isUploading = true; //
//
const formData = {
storeId: getStoreId()
};
// API
importProductData(this.uploadFilePath, formData)
.then(res => {
this.isUploading = false;
// code=200
if (res.code === 200) {
uni.showToast({
title: `导入成功!共导入 ${res.data} 个商品`,
icon: 'success',
duration: 2500
});
//
this.cancelFile();
}
//
else {
uni.showModal({
title: '导入失败',
content: res.msg || '文件内容有误,请检查后重新上传',
showCancel: false,
confirmText: '知道了'
});
}
})
.catch(err => {
this.isUploading = false;
console.error('文件上传失败:', err);
uni.showModal({
title: '上传异常',
content: '网络异常或服务器连接失败,请检查接口服务后重试',
showCancel: false,
confirmText: '重新上传'
});
});
},
//
openLink() {
uni.showModal({
title: '温馨提示',
content: '将在浏览器中打开电脑端录入地址',
confirmText: '确认打开',
success: (res) => {
if (res.confirm) {
uni.openURL({
url: 'http://e.weidian.com/main',
fail: () => uni.showToast({ title: '打开失败,请手动复制链接', icon: 'none' })
});
}
}
});
},
//
goQuickSetup() {
uni.navigateTo({
url: '/pages/quick-setup/quick-setup'
});
},
//
getImportRecord() {
uni.showLoading({ title: '加载中...', mask: true });
// API
getImportRecord()
.then(res => {
if(res.code == 200){
this.recordList = res.data || [];
}
})
.catch(err => {
console.error('获取导入记录失败:', err);
})
.finally(() => {
uni.hideLoading();
});
}
}
}
</script>
<style scoped>
/* 全局样式重置 */
.container {
background-color: #f5f5f5;
min-height: 100vh;
}
/* 导航栏 */
.navbar {
display: flex;
align-items: center;
justify-content: space-between;
background-color: #e62318;
color: white;
padding: 20rpx 30rpx;
box-sizing: border-box;
}
.nav-left, .nav-right {
width: 60rpx;
font-size: 32rpx;
}
.nav-title {
font-size: 34rpx;
font-weight: bold;
}
/* 选项卡 */
.tab-bar {
display: flex;
background-color: white;
}
.tab-item {
flex: 1;
text-align: center;
padding: 30rpx 0;
font-size: 32rpx;
color: #333;
border-bottom: 4rpx solid transparent;
}
.tab-item.active {
color: #e62318;
border-bottom-color: #e62318;
font-weight: bold;
}
/* 内容区 */
.content {
padding: 30rpx;
}
/* 卡片通用样式 */
.card {
background-color: white;
border-radius: 16rpx;
margin-bottom: 30rpx;
overflow: hidden;
}
.card-header {
display: flex;
align-items: center;
padding: 30rpx;
background-color: white;
}
.method-tag {
background-color: #e62318;
color: white;
font-size: 24rpx;
padding: 8rpx 16rpx;
border-radius: 12rpx;
margin-right: 20rpx;
}
.method-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
}
/* 上传区域 */
.upload-area {
margin: 0 30rpx 30rpx;
border: 2rpx dashed #ccc;
border-radius: 12rpx;
padding: 60rpx 30rpx;
text-align: center;
}
.upload-icon {
font-size: 48rpx;
color: #999;
margin-bottom: 20rpx;
}
.upload-text {
font-size: 30rpx;
color: #333;
margin-bottom: 10rpx;
}
.upload-desc {
font-size: 24rpx;
color: #999;
}
/* 已选择文件样式 */
.file-selected {
margin: 0 30rpx 30rpx;
padding: 20rpx;
background: #f9f9f9;
border-radius: 12rpx;
display: flex;
justify-content: space-between;
align-items: center;
}
.file-name {
font-size: 28rpx;
color: #333;
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.file-opt {
display: flex;
gap: 20rpx;
}
.reupload {
font-size: 26rpx;
color: #e62318;
}
.cancel {
font-size: 26rpx;
color: #999;
}
/* 导入按钮样式 */
.import-btn-box {
padding: 0 30rpx 20rpx;
}
.import-btn {
width: 100%;
background: #e62318;
color: white;
font-size: 30rpx;
padding: 20rpx 0;
border-radius: 12rpx;
}
/* 描述列表 */
.desc-list {
padding: 0 30rpx 30rpx;
font-size: 26rpx;
color: #666;
line-height: 1.6;
}
.desc-item {
display: block;
margin-bottom: 10rpx;
}
/* 卡片内容区 */
.card-content {
padding: 0 30rpx 30rpx;
font-size: 28rpx;
color: #666;
line-height: 1.5;
}
.content-text {
margin-bottom: 20rpx;
display: block;
}
.link-area {
display: flex;
align-items: center;
color: #e62318;
}
.link-text {
font-size: 28rpx;
margin-right: 10rpx;
}
/* 导入记录样式 */
.record-content {
padding: 30rpx;
}
.empty-state {
text-align: center;
padding: 100rpx 0;
color: #999;
}
.empty-icon {
font-size: 60rpx;
display: block;
margin-bottom: 20rpx;
}
.empty-text {
font-size: 30rpx;
margin-bottom: 10rpx;
display: block;
}
.empty-subtext {
font-size: 24rpx;
}
.record-list {
background: white;
border-radius: 16rpx;
}
.record-item {
padding: 30rpx;
border-bottom: 1rpx solid #f5f5f5;
}
.record-name {
font-size: 30rpx;
color: #333;
margin-bottom: 10rpx;
}
.record-info {
display: flex;
justify-content: space-between;
font-size: 24rpx;
color: #999;
}
.record-status.success {
color: #07c160;
}
.record-status.fail {
color: #e62318;
}
</style>

View File

@ -0,0 +1,466 @@
<template>
<view class="add-product-page">
<!-- 头部 -->
<view class="page-header">
<text class="header-title">添加商品</text>
</view>
<!-- 表单区域 -->
<view class="form-container">
<!-- 商品名称 -->
<view class="form-item">
<text class="item-label">商品名称</text>
<input
class="item-input"
type="text"
placeholder="农夫山泉东方树叶乌龙茶500ml"
v-model="productInfo.name"
/>
</view>
<!-- 条码/条码扫码 -->
<view class="form-item">
<text class="item-label">条码</text>
<view class="barcode-section">
<input
class="item-input barcode-input"
type="text"
placeholder="6921168558032"
v-model="productInfo.barcode"
/>
<view class="scan-btn" @tap="handleScan">
<text class="scan-text">扫码</text>
</view>
</view>
</view>
<!-- 商品图 -->
<view class="form-item">
<text class="item-label">商品图</text>
<view class="image-upload-section">
<view class="upload-placeholder" @tap="chooseImage">
<text class="upload-icon">+</text>
<text class="upload-text">点击上传商品图片</text>
</view>
<image
v-if="productInfo.image"
:src="productInfo.image"
class="product-image"
mode="aspectFit"
></image>
</view>
</view>
<view class="divider"></view>
<!-- 售价 -->
<view class="form-item">
<text class="item-label">售价</text>
<view class="price-section">
<input
class="item-input price-input"
type="digit"
placeholder="5.0"
v-model="productInfo.price"
/>
<text class="price-unit"></text>
</view>
</view>
<!-- 推荐价格 -->
<view class="recommend-price-section">
<text class="recommend-title">推荐价格点击快速设置售价</text>
<view class="recommend-list">
<view class="recommend-item" @tap="setPrice(5.0)">
<text class="recommend-percent">82%商家卖</text>
<text class="recommend-price">5.0</text>
</view>
<view class="recommend-item" @tap="setPrice(5.5)">
<text class="recommend-percent">9%商家卖</text>
<text class="recommend-price">5.5</text>
</view>
<view class="recommend-item" @tap="setPrice(4.5)">
<text class="recommend-percent">2%商家卖</text>
<text class="recommend-price">4.5</text>
</view>
</view>
</view>
<view class="divider"></view>
<!-- 进货数量 -->
<view class="form-item">
<text class="item-label">进货数量</text>
<view class="quantity-section">
<input
class="item-input"
type="number"
placeholder="请输入进货数量"
v-model="productInfo.quantity"
/>
<text class="quantity-unit"></text>
</view>
</view>
<!-- 进货价 -->
<view class="form-item">
<text class="item-label">进货价</text>
<view class="cost-section">
<input
class="item-input"
type="digit"
placeholder="请输入进货价"
v-model="productInfo.cost"
/>
<text class="cost-unit"></text>
</view>
</view>
<!-- 保质期管理 - 红色滑动开关 完美匹配截图 -->
<view class="form-item">
<text class="item-label">保质期管理</text>
<switch
:checked="productInfo.expirationManagement"
@change="toggleExpirationManagement"
color="#F53F3F"
/>
</view>
<!-- 保质期天数 -->
<view class="form-item" v-if="productInfo.expirationManagement">
<text class="item-label">保质期天数</text>
<view class="expiration-options">
<view
class="expiration-item"
:class="{ active: productInfo.expirationDays === 365 }"
@tap="setExpirationDays(365)"
>
<text class="expiration-text">1</text>
<text class="expiration-days">36</text>
</view>
<view
class="expiration-item"
:class="{ active: productInfo.expirationDays === 270 }"
@tap="setExpirationDays(270)"
>
<text class="expiration-text">9个月</text>
<text class="expiration-days">27</text>
</view>
<view
class="expiration-item"
:class="{ active: productInfo.expirationDays === 180 }"
@tap="setExpirationDays(180)"
>
<text class="expiration-text">6个月</text>
<text class="expiration-days">18</text>
</view>
<view
class="expiration-item"
:class="{ active: productInfo.expirationDays === 240 }"
@tap="setExpirationDays(240)"
>
<text class="expiration-text">8个月</text>
<text class="expiration-days">24</text>
</view>
<view
class="expiration-item"
:class="{ active: productInfo.expirationDays === 300 }"
@tap="setExpirationDays(300)"
>
<text class="expiration-text">10个月</text>
<text class="expiration-days">30</text>
</view>
</view>
</view>
<!-- 临期提醒天数 -->
<view class="form-item" v-if="productInfo.expirationManagement">
<text class="item-label">临期提醒天数</text>
<view class="warning-days-display">
<text class="warning-days-value">{{ warningDaysDisplay }}</text>
<text class="warning-days-unit"></text>
</view>
</view>
<!-- 生产日期 -->
<view class="form-item" v-if="productInfo.expirationManagement">
<text class="item-label">生产日期</text>
<uni-datetime-picker
type="date"
v-model="productInfo.productionDate"
:start="startDate"
:end="endDate"
placeholder="请选择生产日期"
/>
</view>
<!-- 货架码 -->
<view class="form-item">
<text class="item-label">货架码</text>
<input
class="item-input"
type="text"
placeholder="请输入货架码"
v-model="productInfo.shelfCode"
/>
</view>
<!-- 商品编码 -->
<view class="form-item">
<text class="item-label">商品编码</text>
<input
class="item-input"
type="text"
placeholder="请输入商品编码"
v-model="productInfo.productCode"
/>
</view>
</view>
<!-- 保存按钮 -->
<view class="save-button-container">
<button class="save-button" @tap="handleSave"></button>
</view>
</view>
</template>
<script>
import { addProductWithFile } from '@/api/product'
import { getStoreId } from '@/utils/auth'
export default {
data() {
return {
productInfo: {
name: '',
barcode: '',
image: '',
price: '',
quantity: '',
cost: '',
expirationManagement: false,
shelfCode: '',
productCode: '',
expirationDays: '',
warningDays: '',
productionDate: ''
}
}
},
computed: {
warningDaysDisplay() {
if (!this.productInfo.expirationDays) return '';
const days = parseInt(this.productInfo.expirationDays);
return days.toString().substring(0, 2);
},
startDate() {
return '2020-01-01';
},
endDate() {
const now = new Date();
const year = now.getFullYear();
const month = (now.getMonth() + 1).toString().padStart(2, '0');
const day = now.getDate().toString().padStart(2, '0');
return `${year}-${month}-${day}`;
}
},
onLoad(options) {
console.log('页面参数:', options);
if (options.fromData) {
try {
const fromData = JSON.parse(decodeURIComponent(options.fromData));
console.log('接收到的商品数据:', fromData);
this.productInfo.name = fromData.productName || '';
this.productInfo.barcode = fromData.productBarCode || '';
this.productInfo.price = fromData.storePrice || '';
this.productInfo.quantity = fromData.stockQuantity || '';
this.productInfo.productCode = fromData.productCode || '';
this.productInfo.image = fromData.mainImage || '';
} catch (error) {
console.error('解析商品数据失败:', error);
}
}
},
methods: {
//
handleScan() {
uni.showToast({ title: '扫码功能(模拟)', icon: 'none' })
setTimeout(() => {
this.productInfo.barcode = '6921168558032'
uni.showToast({ title: '扫码成功', icon: 'success' })
}, 500)
},
//
chooseImage() {
uni.chooseImage({
count: 1,
sizeType: ['compressed'],
sourceType: ['album', 'camera'],
success: (res) => {
this.productInfo.image = res.tempFilePaths[0]
}
})
},
//
setPrice(price) {
this.productInfo.price = price.toString()
uni.showToast({ title: `已设置为${price}`, icon: 'success', duration: 1500 })
},
// -
toggleExpirationManagement(e) {
this.productInfo.expirationManagement = e.detail.value
},
//
setExpirationDays(days) {
this.productInfo.expirationDays = days.toString()
const warningDays = days.toString().substring(0, 2);
this.productInfo.warningDays = warningDays;
uni.showToast({ title: `已设置为${days}`, icon: 'success', duration: 1500 })
},
// -
validateForm() {
if (!this.productInfo.name.trim()) { uni.showToast({ title: '请输入商品名称', icon: 'none' });return false }
if (!this.productInfo.price) { uni.showToast({ title: '请输入售价', icon: 'none' });return false }
//
if (this.productInfo.expirationManagement) {
if (!this.productInfo.expirationDays) { uni.showToast({ title: '请选择保质期天数', icon: 'none' });return false }
//
if (!this.productInfo.productionDate) { uni.showToast({ title: '请选择生产日期', icon: 'none' });return false }
}
return true
},
// -
async handleSave() {
if (!this.validateForm()) return
const storeId = getStoreId();
if (!storeId) { uni.showToast({ title: '请先选择门店', icon: 'none' });return }
uni.showLoading({ title: '保存中...' });
try {
const formData = {
productName: this.productInfo.name,
productBarCode: this.productInfo.barcode,
storePrice: this.productInfo.price,
stockQuantity: this.productInfo.quantity,
costPrice: this.productInfo.cost,
productCode: this.productInfo.productCode,
shelfCode: this.productInfo.shelfCode,
expirationManagement: this.productInfo.expirationManagement ? 1 : 0,
shelfLife: this.productInfo.expirationDays,
productionDate: this.productInfo.productionDate,
approaching: this.productInfo.warningDays,
storeId: storeId
};
const res = await addProductWithFile(this.productInfo.image || '', formData);
uni.hideLoading();
if (res.code === 200) {
uni.showToast({ title: '商品添加成功', icon: 'success', duration: 2000 });
setTimeout(() => { uni.navigateBack({ delta: 1 }); }, 2000);
} else {
uni.showToast({ title: res.msg || '商品添加失败', icon: 'none' });
}
} catch (error) {
uni.hideLoading();
console.error('添加商品失败:', error);
uni.showToast({ title: '网络请求失败', icon: 'none' });
}
}
}
}
</script>
<style scoped>
.add-product-page {
background-color: #f5f5f5;
min-height: 100vh;
padding-bottom: 120rpx;
}
/* 头部 */
.page-header {
background-color: #ffffff;
padding: 40rpx 32rpx;
border-bottom: 1rpx solid #f0f0f0;
}
.header-title { font-size: 36rpx;font-weight: 600;color: #333333; }
/* 表单容器 */
.form-container { background-color: #ffffff;margin-top: 20rpx; }
/* 表单项 */
.form-item {
padding: 32rpx;
border-bottom: 1rpx solid #f0f0f0;
display: flex;
align-items: center;
justify-content: space-between;
}
.item-label { font-size: 32rpx;color: #333333;font-weight: 500;min-width: 160rpx; }
.item-input { flex:1;font-size:32rpx;color:#333;text-align:right;padding:0 20rpx; }
.item-input::placeholder { color: #999999; }
/* 条码区域 */
.barcode-section { display:flex;align-items:center;flex:1; }
.barcode-input { flex:1;margin-right:20rpx; }
.scan-btn { background:#07C160;padding:16rpx 32rpx;border-radius:8rpx; }
.scan-text { color:#fff;font-size:28rpx; }
/* 图片上传区域 */
.image-upload-section { flex:1;display:flex;justify-content:flex-end; }
.upload-placeholder { width:200rpx;height:200rpx;border:2rpx dashed #ccc;border-radius:8rpx;display:flex;flex-direction:column;align-items:center;justify-content:center;background:#f9f9f9; }
.upload-icon { font-size:48rpx;color:#999;margin-bottom:16rpx; }
.upload-text { font-size:24rpx;color:#999; }
.product-image { width:200rpx;height:200rpx;border-radius:8rpx; }
/* 价格区域 */
.price-section { display:flex;align-items:center;flex:1;justify-content:flex-end; }
.price-input { flex:none;width:120rpx;text-align:center; }
.price-unit { font-size:32rpx;color:#333;margin-left:10rpx; }
/* 推荐价格 */
.recommend-price-section { padding:32rpx;border-bottom:1rpx solid #f0f0f0; }
.recommend-title { font-size:28rpx;color:#666;margin-bottom:24rpx;display:block; }
.recommend-list { display:flex;justify-content:space-between; }
.recommend-item { display:flex;flex-direction:column;align-items:center;padding:20rpx 24rpx;background:#f9f9f9;border-radius:8rpx;flex:1;margin:0 10rpx; }
.recommend-percent { font-size:26rpx;color:#666;margin-bottom:8rpx; }
.recommend-price { font-size:30rpx;color:#07C160;font-weight:500; }
/* 数量/进货价 单位 */
.quantity-section,.cost-section { display:flex;align-items:center;flex:1;justify-content:flex-end; }
.quantity-unit,.cost-unit { font-size:32rpx;color:#333;margin-left:10rpx; }
/* ✅ 保质期相关样式 完美匹配 */
.expiration-options { display:flex;flex-wrap:wrap;gap:20rpx;flex:1;justify-content:flex-end; }
.expiration-item { display:flex;flex-direction:column;align-items:center;padding:16rpx 20rpx;background:#f9f9f9;border-radius:8rpx;min-width:100rpx;border:2rpx solid transparent;transition:all 0.3s; }
.expiration-item.active { background:#F53F3F;border-color:#F53F3F; }
.expiration-item.active .expiration-text { color:#fff; }
.expiration-item.active .expiration-days { color:#fff; }
.expiration-text { font-size:28rpx;color:#333; }
.expiration-days { font-size:24rpx;color:#999;margin-top:4rpx; }
/* 临期提醒天数样式 */
.warning-days-display { display:flex;align-items:center;flex:1;justify-content:flex-end; }
.warning-days-value { font-size:36rpx;color:#F53F3F;font-weight:600;margin-right:8rpx; }
.warning-days-unit { font-size:28rpx;color:#666; }
/* 分隔线 */
.divider { height:20rpx;background:#f5f5f5;border-top:1rpx solid #f0f0f0;border-bottom:1rpx solid #f0f0f0; }
/* 保存按钮 */
.save-button-container { position:fixed;bottom:0;left:0;right:0;background:#fff;padding:20rpx 32rpx;border-top:1rpx solid #f0f0f0; }
.save-button { background:#F53F3F;color:#ffffff;font-size:34rpx;height:88rpx;line-height:88rpx;border-radius:8rpx; }
.save-button::after { border:none; }
/* 响应式调整 */
@media (max-width:750px) {
.recommend-list { flex-direction:column; }
.recommend-item { margin:10rpx 0; }
}
</style>

284
pages/asset/asset.vue Normal file
View File

@ -0,0 +1,284 @@
<template>
<view class="page">
<!-- 顶部资产卡片 -->
<view class="asset-card">
<view class="asset-title">我的资产</view>
<view class="asset-amount">¥0.00</view>
<view class="asset-details">
<view class="detail-item">
<view class="label">可提现</view>
<view class="value">¥0.00</view>
</view>
<view class="divider"></view>
<view class="detail-item">
<view class="label">待结算</view>
<view class="value">¥0.00</view>
<view class="hint">次日可提现</view>
</view>
</view>
</view>
<!-- 操作按钮 -->
<view class="action-buttons">
<button class="btn primary" @tap="handleRecharge"></button>
<button class="btn outline" @tap="handleWithdraw"></button>
</view>
<!-- 资金明细标题 -->
<view class="section-title">资金明细</view>
<!-- 明细列表 -->
<view class="detail-list">
<view class="list-item" @tap="goToDetail('withdrawable')">
<view class="item-left">
<view class="item-icon withdrawable">可提现</view>
<view class="item-label">¥0.00</view>
</view>
<view class="item-arrow">></view>
</view>
<view class="list-item active" @tap="goToDetail('pending')">
<view class="item-left">
<view class="item-icon pending">待结算()</view>
<view class="item-label">¥0.00</view>
</view>
<view class="item-arrow"></view>
</view>
<view class="list-item" @tap="goToDetail('withdrawn')">
<view class="item-left">
<view class="item-icon withdrawn">已提现()</view>
<view class="item-label">¥0.00</view>
</view>
<view class="item-arrow">></view>
</view>
<view class="list-item" @tap="goToDetail('cash')">
<view class="item-left">
<view class="item-icon cash">现金支付()</view>
<view class="item-label">¥0.00</view>
</view>
<view class="item-arrow">></view>
</view>
<view class="list-item" @tap="goToBankCard">
<view class="item-left">
<view class="item-icon bank">我的银行卡</view>
<view class="item-label">未绑定</view>
</view>
<view class="item-arrow">></view>
</view>
</view>
<!-- 常见问题 -->
<view class="faq-section">
<view class="faq-title">常见问题</view>
<!-- 这里可以展开常见问题列表 -->
</view>
</view>
</template>
<script setup>
const handleRecharge = () => {
uni.showToast({
title: '充值功能',
icon: 'none'
})
}
const handleWithdraw = () => {
uni.showToast({
title: '提现功能',
icon: 'none'
})
}
const goToDetail = (type) => {
uni.showToast({
title: `查看${type}明细`,
icon: 'none'
})
}
const goToBankCard = () => {
uni.showToast({
title: '前往绑定银行卡',
icon: 'none'
})
}
</script>
<style lang="scss" scoped>
.page {
padding: 20rpx 30rpx;
background-color: #f8f8f8;
min-height: 100vh;
}
//
.asset-card {
background: linear-gradient(135deg, #4a6ee0, #6a8eff);
border-radius: 20rpx;
padding: 40rpx 30rpx;
color: #fff;
margin-bottom: 40rpx;
box-shadow: 0 10rpx 20rpx rgba(74, 110, 224, 0.2);
}
.asset-title {
font-size: 28rpx;
opacity: 0.9;
margin-bottom: 20rpx;
}
.asset-amount {
font-size: 60rpx;
font-weight: bold;
margin-bottom: 50rpx;
}
.asset-details {
display: flex;
justify-content: space-between;
align-items: center;
}
.detail-item {
flex: 1;
text-align: center;
}
.label {
font-size: 26rpx;
opacity: 0.85;
margin-bottom: 10rpx;
}
.value {
font-size: 36rpx;
font-weight: bold;
}
.hint {
font-size: 24rpx;
opacity: 0.7;
margin-top: 8rpx;
}
.divider {
width: 2rpx;
height: 80rpx;
background-color: rgba(255, 255, 255, 0.3);
}
//
.action-buttons {
display: flex;
gap: 20rpx;
margin-bottom: 50rpx;
}
.btn {
flex: 1;
height: 90rpx;
border-radius: 45rpx;
font-size: 30rpx;
font-weight: 500;
border: none;
display: flex;
align-items: center;
justify-content: center;
&.primary {
background-color: #4a6ee0;
color: #fff;
}
&.outline {
background-color: transparent;
border: 2rpx solid #4a6ee0;
color: #4a6ee0;
}
}
//
.section-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
margin-bottom: 30rpx;
padding-left: 10rpx;
}
//
.detail-list {
background-color: #fff;
border-radius: 20rpx;
padding: 0 30rpx;
margin-bottom: 40rpx;
box-shadow: 0 5rpx 15rpx rgba(0, 0, 0, 0.03);
}
.list-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 30rpx 0;
border-bottom: 1rpx solid #f0f0f0;
&:last-child {
border-bottom: none;
}
&.active {
.item-icon {
color: #4a6ee0;
}
.item-label {
color: #4a6ee0;
}
}
}
.item-left {
display: flex;
align-items: center;
gap: 20rpx;
}
.item-icon {
font-size: 30rpx;
color: #333;
&.withdrawable::before { content: "💳 "; }
&.pending::before { content: "⏳ "; }
&.withdrawn::before { content: "💰 "; }
&.cash::before { content: "💵 "; }
&.bank::before { content: "🏦 "; }
}
.item-label {
font-size: 28rpx;
color: #666;
}
.item-arrow {
font-size: 32rpx;
color: #999;
}
//
.faq-section {
background-color: #fff;
border-radius: 20rpx;
padding: 30rpx;
box-shadow: 0 5rpx 15rpx rgba(0, 0, 0, 0.03);
}
.faq-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
}
</style>

View File

@ -0,0 +1,43 @@
<template>
<view>
<uni-card class="view-title" :title="title">
<text class="uni-body view-content">{{ content }}</text>
</uni-card>
</view>
</template>
<script>
export default {
data() {
return {
title: '',
content: ''
}
},
onLoad(options) {
this.title = options.title
this.content = options.content
uni.setNavigationBarTitle({
title: options.title
})
}
}
</script>
<style scoped>
page {
background-color: #ffffff;
}
.view-title {
font-weight: bold;
}
.view-content {
font-size: 26rpx;
padding: 12px 5px 0;
color: #333;
line-height: 24px;
font-weight: normal;
}
</style>

View File

@ -0,0 +1,34 @@
<template>
<view v-if="params.url">
<web-view :webview-styles="webviewStyles" :src="`${params.url}`"></web-view>
</view>
</template>
<script>
export default {
data() {
return {
params: {},
webviewStyles: {
progress: {
color: "#FF3333"
}
}
}
},
props: {
src: {
type: [String],
default: null
}
},
onLoad(event) {
this.params = event
if (event.title) {
uni.setNavigationBarTitle({
title: event.title
})
}
}
}
</script>

892
pages/edit/edit.vue Normal file
View File

@ -0,0 +1,892 @@
<template>
<view class="goods-edit-page">
<!-- 顶部导航栏 -->
<view class="navbar">
<view class="nav-left" @click="navBack">
<text class="icon"></text>
</view>
<view class="nav-title">编辑商品</view>
<view class="nav-right">
<text class="icon">···</text>
</view>
</view>
<!-- 商品图片展示区 -->
<view class="goods-img-box">
<image
class="goods-img"
:src="goodsInfo.imageUrl"
mode="widthFix"
@click="previewImage"
></image>
<view class="change-img-btn" @click="chooseImage"></view>
</view>
<!-- 核心表单区域 -->
<view class="form-container">
<!-- 商品名称 -->
<view class="form-item">
<view class="form-label">
<text class="required">*</text>名称
</view>
<input
class="item-input"
type="text"
placeholder="请输入商品名称"
v-model="goodsInfo.name"
/>
</view>
<!-- 商品单位 -->
<view class="form-item">
<view class="form-label">商品单位</view>
<input
class="item-input"
type="text"
placeholder="请输入商品单位"
v-model="goodsInfo.unit"
/>
</view>
<!-- 条码+扫码按钮 -->
<view class="form-item">
<view class="form-label">
<text class="required">*</text>条码
</view>
<view class="flex-row">
<input
class="item-input barcode-input"
type="text"
placeholder="请输入条码"
v-model="goodsInfo.barcode"
/>
<view class="scan-btn" @click="scanBarcode"></view>
</view>
</view>
<!-- 售价核心区域修正排版 -->
<view class="form-item">
<view class="form-label">
<text class="required">*</text>售价
</view>
<view class="flex-row">
<input
class="item-input price-input"
type="digit"
placeholder="请输入售价"
v-model="goodsInfo.salePrice"
/>
<text class="price-unit"></text>
</view>
</view>
<!-- 推荐价格提示与选项 -->
<view class="price-recommend-section">
<view class="price-tips">
推荐价格点击快速设置售价<text class="tips-icon">i</text>
</view>
<view class="price-option-list">
<view
class="price-option"
:class="{active: activePrice == item.price}"
v-for="item in priceOptions"
:key="item.price"
@click="setSalePrice(item.price)"
>
<text class="option-text">{{ item.desc }}</text>
<text class="option-price">{{ item.price }}</text>
</view>
</view>
</view>
<!-- 库存 -->
<view class="form-item">
<view class="form-label">库存</view>
<view class="flex-row">
<input
class="item-input"
type="number"
placeholder="请输入库存"
v-model="goodsInfo.stock"
/>
<text class="unit-text"></text>
</view>
</view>
<!-- 最近进货价 -->
<view class="form-item">
<view class="form-label">最近进货价</view>
<view class="flex-row">
<input
class="item-input"
type="digit"
placeholder="请输入进货价"
v-model="goodsInfo.buyPrice"
/>
<text class="unit-text"></text>
</view>
</view>
<!-- 货架码 -->
<view class="form-item">
<view class="form-label">货架码</view>
<input
class="item-input"
type="text"
placeholder="请输入货架码"
v-model="goodsInfo.shelfCode"
/>
</view>
<!-- 添加更多单位按钮 -->
<view class="add-unit-btn" @click="addUnit">+ </view>
</view>
<!-- 其他设置区域 -->
<view class="settings-container">
<view class="settings-title">其他设置</view>
<!-- 保质期管理 -->
<view class="form-item">
<text class="item-label">保质期管理</text>
<switch
:checked="switchStatus.expire"
@change="toggleExpirationManagement"
color="#F53F3F"
/>
</view>
<!-- 保质期天数 -->
<view class="form-item" v-if="switchStatus.expire">
<text class="item-label">保质期天数</text>
<view class="expiration-options">
<view
class="expiration-item"
:class="{ active: expireSettings.days === 365 }"
@tap="setExpirationDays(365)"
>
<text class="expiration-text">1</text>
<text class="expiration-days">365</text>
</view>
<view
class="expiration-item"
:class="{ active: expireSettings.days === 270 }"
@tap="setExpirationDays(270)"
>
<text class="expiration-text">9个月</text>
<text class="expiration-days">270</text>
</view>
<view
class="expiration-item"
:class="{ active: expireSettings.days === 180 }"
@tap="setExpirationDays(180)"
>
<text class="expiration-text">6个月</text>
<text class="expiration-days">180</text>
</view>
<view
class="expiration-item"
:class="{ active: expireSettings.days === 240 }"
@tap="setExpirationDays(240)"
>
<text class="expiration-text">8个月</text>
<text class="expiration-days">240</text>
</view>
<view
class="expiration-item"
:class="{ active: expireSettings.days === 300 }"
@tap="setExpirationDays(300)"
>
<text class="expiration-text">10个月</text>
<text class="expiration-days">300</text>
</view>
</view>
</view>
<!-- 临期提醒天数 -->
<view class="form-item" v-if="switchStatus.expire">
<text class="item-label">临期提醒天数</text>
<view class="flex-row">
<input
class="item-input"
type="number"
placeholder="请输入临期提醒天数"
v-model="expireSettings.warnDays"
/>
<text class="unit-text"></text>
</view>
</view>
<!-- 未成年人购买香烟提示 -->
<view class="setting-item flex-row-between">
<view class="setting-label flex-row">
<text>未成年人购买香烟提示</text>
<text class="tips-icon">i</text>
</view>
<view class="switch-box" @click="toggleSwitch('minor')">
<view class="switch-bg" :class="{open: switchStatus.minor}"></view>
<view class="switch-btn" :class="{open: switchStatus.minor}"></view>
</view>
</view>
<!-- 商品分类 -->
<view class="setting-item flex-row-between">
<view class="setting-label">商品分类</view>
<input
class="item-input"
type="text"
placeholder="请输入商品分类"
v-model="goodsInfo.category"
/>
</view>
<!-- 商品品牌 -->
<view class="setting-item flex-row-between">
<view class="setting-label">商品品牌</view>
<input
class="item-input"
type="text"
placeholder="请输入商品品牌"
v-model="goodsInfo.brand"
/>
</view>
<!-- 商品编码 -->
<view class="setting-item flex-row-between">
<view class="setting-label">商品编码</view>
<input
class="item-input"
type="text"
placeholder="请输入商品编码"
v-model="goodsInfo.code"
/>
</view>
</view>
<!-- 底部固定保存按钮 -->
<view class="bottom-submit">
<button class="save-btn" @click="saveGoods"></button>
</view>
</view>
</template>
<script>
import { getProductDetail, updateProduct, updateProductWithFile } from '@/api/product'
import { getStoreId } from '@/utils/auth'
export default {
name: 'GoodsEdit',
data() {
return {
productId: null,
goodsInfo: {
imageUrl: 'https://p3-flow-imagex-sign.byteimg.com/tos-cn-i-a9rns2rl98/1b5ce0f17f5e4942a350d4c58f83dde9.png~tplv-a9rns2rl98-image.png?lk3s=8e244e95&rcl=202601151851433FA5618C22F61C91356F&rrcfp=dafada99&x-expires=2084698303&x-signature=mApXMrIjzYh%2B1B6IE2pHDPy9Z2U%3D',
name: '农夫山泉饮用天然水 550ML',
unit: '瓶',
barcode: '6921168509256',
salePrice: 5.5,
stock: 1,
buyPrice: 1.0,
category: '默认',
brand: '默认品牌',
code: '1',
shelfCode: '',
productionDate: ''
},
priceOptions: [
{ price: 2.0, desc: '92%商家卖' },
{ price: 1.5, desc: '2%商家卖' },
{ price: 1.8, desc: '1%商家卖' }
],
activePrice: 2.0,
switchStatus: {
expire: true,
minor: false
},
expireSettings: {
days: 240,
warnDays: 24,
activeOption: { label: '8个月', days: 240 }
},
expireOptionList: [
{ label: '1年', days: 365 },
{ label: '9个月', days: 270 },
{ label: '6个月', days: 180 },
{ label: '8个月', days: 240 },
{ label: '10个月', days: 300 }
]
}
},
onLoad(options) {
console.log('编辑页面参数:', options);
if (options.id) {
this.productId = options.id;
} else {
uni.showToast({
title: '缺少商品ID参数',
icon: 'none'
});
}
},
onShow() {
if (this.productId) {
this.loadProductDetail();
}
},
computed: {
warningDaysDisplay() {
if (!this.expireSettings.days) return '';
const days = parseInt(this.expireSettings.days);
return days.toString().substring(0, 2);
}
},
methods: {
navBack() {
uni.navigateBack({ delta: 1 });
},
previewImage() {
uni.previewImage({
urls: [this.goodsInfo.imageUrl],
current: this.goodsInfo.imageUrl
});
},
chooseImage() {
uni.chooseImage({
count: 1,
sizeType: ['original', 'compressed'],
sourceType: ['album', 'camera'],
success: (res) => {
this.goodsInfo.imageUrl = res.tempFilePaths[0];
}
});
},
scanBarcode() {
uni.scanCode({
onlyFromCamera: true,
scanType: ['barCode'],
success: (res) => {
this.goodsInfo.barcode = res.result;
uni.showToast({ title: '扫码成功', icon: 'success' });
},
fail: () => {
uni.showToast({ title: '扫码取消', icon: 'none' });
}
});
},
setSalePrice(price) {
this.activePrice = price;
this.goodsInfo.salePrice = price;
},
toggleSwitch(type) {
if (type === 'expire') {
this.switchStatus.expire = !this.switchStatus.expire;
} else if (type === 'minor') {
this.switchStatus.minor = !this.switchStatus.minor;
}
},
toggleExpirationManagement(e) {
this.switchStatus.expire = e.detail.value;
},
setExpirationDays(days) {
this.expireSettings.days = days;
const warningDays = days.toString().substring(0, 2);
this.expireSettings.warnDays = warningDays;
uni.showToast({ title: `已设置为${days}`, icon: 'success', duration: 1500 });
},
async loadProductDetail() {
if (!this.productId) {
uni.showToast({
title: '缺少商品ID',
icon: 'none'
});
return;
}
try {
console.log('正在获取商品详情ID:', this.productId);
const res = await getProductDetail(this.productId);
console.log('商品详情接口返回:', res);
if (res && res.code === 200 && res.data) {
const data = res.data;
this.goodsInfo = {
imageUrl: data.mainImage ? 'http://193.112.94.36:8081' + data.mainImage : this.goodsInfo.imageUrl,
name: data.productName || this.goodsInfo.name,
unit: this.goodsInfo.unit,
barcode: data.productBarCode || this.goodsInfo.barcode,
salePrice: data.storePrice || this.goodsInfo.salePrice,
stock: data.stockQuantity || this.goodsInfo.stock,
buyPrice: data.costPrice || this.goodsInfo.buyPrice,
category: this.goodsInfo.category,
brand: this.goodsInfo.brand,
code: data.productCode || this.goodsInfo.code
};
this.activePrice = data.storePrice || this.goodsInfo.salePrice;
if (data.shelfLife) {
this.switchStatus.expire = true;
this.expireSettings.days = data.shelfLife;
this.expireSettings.warnDays = data.approaching || 24;
const days = data.shelfLife;
const option = this.expireOptionList.find(item => item.days === days);
if (option) {
this.expireSettings.activeOption = option;
}
} else {
this.switchStatus.expire = false;
}
console.log('商品详情数据:', this.goodsInfo);
} else {
uni.showToast({
title: res?.msg || '获取商品详情失败',
icon: 'none'
});
}
} catch (error) {
console.error('获取商品详情失败:', error);
uni.showToast({
title: '网络请求失败',
icon: 'none'
});
}
},
setExpireDays(item) {
this.expireSettings.days = item.days;
this.expireSettings.activeOption = item;
},
addUnit() {
uni.showModal({
title: '提示',
content: '是否添加商品多单位规格?',
success: (res) => {
if (res.confirm) uni.showToast({ title: '添加成功', icon: 'success' });
}
});
},
async saveGoods() {
if (!this.goodsInfo.name || !this.goodsInfo.name.trim()) {
uni.showToast({ title: '请输入商品名称', icon: 'none' });
return;
}
if (!this.goodsInfo.salePrice) {
uni.showToast({ title: '请输入售价', icon: 'none' });
return;
}
if (this.switchStatus.expire && !this.expireSettings.days) {
uni.showToast({ title: '请选择保质期天数', icon: 'none' });
return;
}
const storeId = getStoreId();
if (!storeId) {
uni.showToast({ title: '请先选择门店', icon: 'none' });
return;
}
uni.showLoading({ title: '保存中...' });
try {
const formData = {
id: this.productId,
productName: this.goodsInfo.name,
productBarCode: this.goodsInfo.barcode,
storePrice: this.goodsInfo.salePrice,
stockQuantity: this.goodsInfo.stock,
costPrice: this.goodsInfo.buyPrice,
productCode: this.goodsInfo.code,
shelfCode: this.goodsInfo.shelfCode || '',
expirationManagement: this.switchStatus.expire ? 1 : 0,
shelfLife: this.switchStatus.expire ? this.expireSettings.days : null,
approaching: this.switchStatus.expire ? this.expireSettings.warnDays : null,
productionDate: this.goodsInfo.productionDate || '',
storeId: storeId
};
const res = await updateProductWithFile(this.goodsInfo.imageUrl || '', formData);
uni.hideLoading();
if (res.code === 200) {
uni.showToast({
title: '保存成功',
icon: 'success',
duration: 2000
});
setTimeout(() => {
this.navBack();
}, 2000);
} else {
uni.showToast({
title: res.msg || '保存失败',
icon: 'none'
});
}
} catch (error) {
uni.hideLoading();
console.error('保存商品失败:', error);
uni.showToast({
title: '网络请求失败',
icon: 'none'
});
}
}
}
};
</script>
<style lang="scss" scoped>
//
.goods-edit-page {
background-color: #f5f5f5;
min-height: 100vh;
box-sizing: border-box;
padding-bottom: 120rpx;
}
.flex-row {
display: flex;
align-items: center;
}
.flex-row-between {
display: flex;
align-items: center;
justify-content: space-between;
}
//
.navbar {
height: 88rpx;
background: #E62429;
color: #FFFFFF;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 30rpx;
position: sticky;
top: 0;
left: 0;
right: 0;
z-index: 99;
.nav-left, .nav-right {
width: 60rpx;
text-align: center;
.icon {
font-size: 32rpx;
}
}
.nav-title {
font-size: 36rpx;
font-weight: 500;
}
}
//
.goods-img-box {
background: #FFFFFF;
padding: 40rpx 0;
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 20rpx;
.goods-img {
width: 220rpx;
height: 220rpx;
border-radius: 8rpx;
margin-bottom: 30rpx;
}
.change-img-btn {
font-size: 28rpx;
color: #666666;
padding: 12rpx 30rpx;
border: 1px solid #E5E5E5;
border-radius: 6rpx;
}
}
//
.form-container {
background: #FFFFFF;
margin-bottom: 20rpx;
.form-item {
padding: 30rpx;
border-bottom: 1px solid #F0F0F0;
display: flex;
justify-content: space-between;
align-items: center;
.form-label {
font-size: 30rpx;
color: #333333;
.required {
color: #E62429;
margin-right: 4rpx;
}
}
.form-value {
font-size: 30rpx;
color: #333333;
}
.scan-btn {
margin-left: 20rpx;
padding: 8rpx 20rpx;
background: #F5F5F5;
border-radius: 4rpx;
font-size: 26rpx;
color: #666666;
}
}
//
.price-recommend-section {
padding: 0 30rpx 30rpx;
.price-tips {
width: 100%;
font-size: 24rpx;
color: #999999;
margin-bottom: 15rpx;
.tips-icon {
display: inline-block;
width: 20rpx;
height: 20rpx;
border: 1px solid #999;
border-radius: 50%;
text-align: center;
line-height: 20rpx;
font-size: 18rpx;
margin-left: 5rpx;
}
}
.price-option-list {
width: 100%;
display: flex;
gap: 20rpx;
.price-option {
flex: 1;
background: #F5F5F5;
border-radius: 8rpx;
padding: 20rpx 10rpx;
text-align: center;
.option-text {
font-size: 24rpx;
color: #666;
}
.option-price {
display: block;
font-size: 32rpx;
color: #E62429;
font-weight: bold;
margin-top: 8rpx;
}
&.active {
background: #E62429;
.option-text, .option-price {
color: #FFFFFF;
}
}
}
}
}
.form-placeholder {
font-size: 28rpx;
color: #CCCCCC;
}
.right-arrow {
font-size: 28rpx;
color: #CCCCCC;
margin-left: 10rpx;
}
.add-unit-btn {
margin: 30rpx;
padding: 20rpx;
text-align: center;
font-size: 28rpx;
color: #666666;
border: 1px dashed #CCCCCC;
border-radius: 8rpx;
background: #FFFFFF;
}
}
//
.settings-container {
background: #FFFFFF;
.settings-title {
font-size: 32rpx;
font-weight: bold;
color: #333333;
padding: 30rpx;
border-bottom: 1px solid #F0F0F0;
}
.setting-item {
padding: 30rpx;
border-bottom: 1px solid #F0F0F0;
.setting-label {
font-size: 30rpx;
color: #333333;
.tips-icon {
display: inline-block;
width: 20rpx;
height: 20rpx;
border: 1px solid #999;
border-radius: 50%;
text-align: center;
line-height: 20rpx;
font-size: 18rpx;
margin-left: 5rpx;
}
}
.setting-left {
.setting-tips {
font-size: 24rpx;
color: #1677FF;
margin-top: 8rpx;
}
}
.setting-value {
font-size: 30rpx;
color: #333333;
}
.switch-box {
width: 70rpx;
height: 40rpx;
background: #E5E5E5;
border-radius: 20rpx;
position: relative;
display: flex;
align-items: center;
padding: 0 4rpx;
.switch-bg {
width: 100%;
height: 100%;
position: absolute;
left: 0;
top: 0;
border-radius: 20rpx;
background: #E5E5E5;
transition: all 0.3s ease;
&.open {
background: #E62429;
}
}
.switch-btn {
width: 32rpx;
height: 32rpx;
background: #FFFFFF;
border-radius: 50%;
position: relative;
z-index: 2;
transition: all 0.3s ease;
&.open {
transform: translateX(30rpx);
}
}
}
}
.expire-options {
display: flex;
padding: 0 30rpx 30rpx;
gap: 15rpx;
overflow-x: auto;
.expire-option {
flex-shrink: 0;
background: #F5F5F5;
border: 2px solid transparent;
border-radius: 8rpx;
padding: 15rpx 20rpx;
text-align: center;
position: relative;
.option-text {
font-size: 28rpx;
color: #333;
}
.option-days {
display: block;
font-size: 24rpx;
color: #E62429;
margin-top: 5rpx;
}
&.active {
border-color: #E62429;
background: #FFFFFF;
.checked-icon {
display: block;
position: absolute;
top: -10rpx;
right: -10rpx;
width: 30rpx;
height: 30rpx;
background: #E62429;
color: #FFFFFF;
border-radius: 50%;
font-size: 20rpx;
line-height: 30rpx;
text-align: center;
}
}
}
}
}
/* 保质期相关样式 */
.expiration-options { display:flex;flex-wrap:wrap;gap:20rpx;flex:1;justify-content:flex-end; }
.expiration-item { display:flex;flex-direction:column;align-items:center;padding:16rpx 20rpx;background:#f9f9f9;border-radius:8rpx;min-width:100rpx;border:2rpx solid transparent;transition:all 0.3s; }
.expiration-item.active { background:#F53F3F;border-color:#F53F3F; }
.expiration-item.active .expiration-text { color:#fff; }
.expiration-item.active .expiration-days { color:#fff; }
.expiration-text { font-size:28rpx;color:#333; }
.expiration-days { font-size:24rpx;color:#333;margin-top:4rpx; }
/* 临期提醒天数样式 */
.warning-days-display { display:flex;align-items:center;flex:1;justify-content:flex-end; }
.warning-days-value { font-size:36rpx;color:#333;font-weight:600;margin-right:8rpx; }
.warning-days-unit { font-size:28rpx;color:#333; }
/* 表单项样式 */
.form-item {
padding: 32rpx;
border-bottom: 1rpx solid #f0f0f0;
display: flex;
align-items: center;
justify-content: space-between;
}
.item-label { font-size: 32rpx;color: #333333;font-weight: 500;min-width: 160rpx; }
.item-input { flex:1;font-size:32rpx;color:#333;text-align:right;padding:0 20rpx; }
.item-input::placeholder { color: #999999; }
.barcode-input { flex: 1; }
.price-input { flex: 1; }
.price-unit { font-size: 32rpx;color: #333;margin-left: 10rpx; }
.unit-text { font-size: 32rpx;color: #333;margin-left: 10rpx; }
//
.bottom-submit {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: #FFFFFF;
padding: 20rpx 30rpx;
border-top: 1px solid #F0F0F0;
z-index: 90;
.save-btn {
width: 100%;
height: 90rpx;
line-height: 90rpx;
background: #E62429;
color: #FFFFFF;
font-size: 32rpx;
font-weight: 500;
border-radius: 8rpx;
padding: 0;
margin: 0;
&::after {
border: none;
}
}
}
</style>

676
pages/enter/enter.vue Normal file
View File

@ -0,0 +1,676 @@
<template>
<view class="input-list-page">
<!-- 顶部导航栏 -->
<view class="navbar">
<view class="nav-left" @click="goBack">
<uni-icons type="left" size="20" color="#fff"></uni-icons>
</view>
<view class="nav-title">录入清单</view>
<view class="nav-right" @click="goRecord">
<uni-icons type="redo" size="18" color="#fff"></uni-icons>
<text class="record-text">记录</text>
</view>
</view>
<!-- 功能操作栏 -->
<view class="operation-bar">
<view class="search-box">
<uni-icons type="search" size="16" color="#999"></uni-icons>
<input type="text" placeholder="搜索商品名称、条码" placeholder-class="search-placeholder" />
</view>
<button class="btn batch-import" @click="goImport"></button>
<button class="btn no-code">无码/生鲜</button>
</view>
<!-- 条码扫描区域 修复调整定位和层级 -->
<view class="scan-area">
<view class="scan-tip">对准商品条码自动识别</view>
<!-- App环境使用扫码控件 这里的view只做占位扫码控件是原生图层叠加 -->
<!-- #ifdef APP-PLUS -->
<view class="scan-view" id="barcode-view"></view>
<view class="scan-status" v-if="isScanning">
<view class="scan-line"></view>
<text class="scan-status-text">正在扫描...</text>
</view>
<view class="scan-controls" v-if="isScanning">
<view class="control-item" @click.stop="toggleFlash">
<uni-icons type="flash" size="22" color="#fff"></uni-icons>
<text class="control-text">{{ flashOn ? '关闭手电' : '开启手电' }}</text>
</view>
<view class="control-item" @click.stop="pauseScan">
<uni-icons type="pause" size="22" color="#fff"></uni-icons>
<text class="control-text">{{ scanPaused ? '继续扫码' : '暂停扫码' }}</text>
</view>
<view class="control-item" @click.stop="openFullscreenScan">
<uni-icons type="scan" size="22" color="#fff"></uni-icons>
<text class="control-text">全屏扫码</text>
</view>
</view>
<!-- #endif -->
<!-- H5环境显示提示 -->
<!-- #ifndef APP-PLUS -->
<view class="h5-tip">
<text class="tip-text">扫码功能需要在App中使用</text>
<button class="scan-btn" @click="startScan"></button>
</view>
<!-- #endif -->
</view>
<!-- 空状态区域 -->
<view class="empty-area">
<image src="/static/empty-icon.png" mode="widthFix" class="empty-icon"></image>
<text class="empty-title">暂未录入商品</text>
<text class="empty-desc">请在上方选择商品录入方式</text>
<uni-icons type="arrowup" size="14" color="#ccc"></uni-icons>
</view>
</view>
</template>
<script>
export default {
data() {
return {
flashOn: false,
scanPaused: false,
isScanning: false,
barcodeInstance: null
};
},
// mountedDOM
mounted() {
// #ifdef APP-PLUS
console.log('=== 扫码页面 mounted ===');
this.initScan();
// #endif
},
onShow() {
// #ifdef APP-PLUS
console.log('=== 扫码页面 onShow ===');
//
this.initScan();
// #endif
},
onUnload() {
this.destroyScan(); //
},
methods: {
goBack() {
this.destroyScan(); //
uni.navigateBack({ delta: 1 });
},
goRecord() {
uni.showToast({ title: '记录功能开发中', icon: 'none' });
},
goImport() {
uni.navigateTo({
url: '/pages/Import /Import '
});
},
//
initScan() {
console.log('开始初始化扫码流程...');
console.log('当前平台:', uni.getSystemInfoSync().platform);
console.log('plus对象是否存在:', !!plus);
console.log('plus.barcode是否存在:', !!(plus && plus.barcode));
//
this.requestCameraAuth().then(() => {
console.log('相机权限已获取,开始初始化扫码');
this.initBarcodeScan();
}).catch(err => {
console.error('相机权限获取失败:', err);
//
uni.showModal({
title: '权限提示',
content: '需要相机权限才能使用扫码功能\n\n请点击"去设置"开启相机权限',
confirmText: '去设置',
cancelText: '取消',
success: (res) => {
if (res.confirm) {
//
if (uni.getSystemInfoSync().platform === 'android') {
const Intent = plus.android.importClass('android.content.Intent');
const Settings = plus.android.importClass('android.provider.Settings');
const Uri = plus.android.importClass('android.net.Uri');
const intent = new Intent();
intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
const uri = Uri.fromParts('package', plus.android.runtimeActivity.getPackageName(), null);
intent.setData(uri);
plus.android.runtimeMainActivity().startActivity(intent);
} else {
// iOS
plus.runtime.openURL('app-settings:');
}
} else {
//
uni.navigateBack({ delta: 1 });
}
}
});
});
},
//
destroyScan() {
if (this.barcodeInstance) {
this.barcodeInstance.close();
this.barcodeInstance = null;
}
this.isScanning = false;
this.scanPaused = false;
this.flashOn = false;
},
// Android iOS
requestCameraAuth() {
return new Promise((resolve, reject) => {
try {
const platform = uni.getSystemInfoSync().platform;
console.log('当前平台:', platform);
if (platform === 'android') {
// Android
console.log('Android 平台,开始申请相机权限...');
//
const main = plus.android.runtimeMainActivity();
const Permission = plus.android.importClass('android.Manifest.permission');
const PackageManager = plus.android.importClass('android.content.pm.PackageManager');
const permission = Permission.CAMERA;
const result = main.checkSelfPermission(permission);
console.log('Android 权限检查结果:', result);
if (result === PackageManager.PERMISSION_GRANTED) {
console.log('Android 已有相机权限');
resolve();
} else {
console.log('Android 需要申请相机权限');
plus.android.requestPermissions(
['android.permission.CAMERA'],
(res) => {
console.log('Android 权限请求回调:', res);
if (res && res.length > 0) {
const authResult = res[0].granted;
console.log('Android 相机权限授予结果:', authResult);
if (authResult) {
resolve();
} else {
reject('用户拒绝了相机权限');
}
} else {
reject('权限请求返回异常');
}
},
(err) => {
console.error('Android 权限请求失败:', err);
reject(err);
}
);
}
} else if (platform === 'ios') {
// iOS
console.log('iOS 平台,相机权限在使用时自动申请');
// iOS 使
//
resolve();
} else {
console.log('非移动端平台,跳过权限申请');
resolve();
}
} catch (error) {
console.error('权限申请异常:', error);
reject(error);
}
});
},
//
initBarcodeScan() {
if (!plus) {
console.log('plus 对象不存在,无法初始化扫码');
return;
}
//
if (this.barcodeInstance) {
console.log('已存在扫码实例,先销毁再重新创建');
this.destroyScan();
}
console.log('开始初始化扫码控件');
this.isScanning = true;
this.scanPaused = false;
//
const sysInfo = uni.getSystemInfoSync();
const statusBarHeight = sysInfo.statusBarHeight || 0;
const navBarHeight = 44;
const totalNavBarHeight = statusBarHeight + navBarHeight;
const scanWidth = sysInfo.windowWidth - 30; // 15px
const scanHeight = sysInfo.windowWidth * 0.7; //
const scanTop = totalNavBarHeight + 80; // ++
console.log('系统信息:', {
statusBarHeight,
navBarHeight,
totalNavBarHeight,
scanTop,
scanWidth,
scanHeight
});
// px
const barcode = plus.barcode.create('barcode',
[plus.barcode.CODE_128, plus.barcode.EAN_13, plus.barcode.EAN_8, plus.barcode.QR], // 广
{
top: scanTop,
left: 15,
width: scanWidth,
height: scanHeight,
scanbarColor: '#e60012', // 线
background: '#000',
frameColor: '#e60012', //
scanbarRate: 2, // 线
scanbarStyle: 'style-radar', // 线
conserve: true, //
filename: '_doc/barcode/' //
}
);
console.log('扫码控件创建成功');
//
barcode.onmarked = (type, result) => {
if (!result) return; //
console.log('识别成功:', result);
this.isScanning = false;
uni.showModal({
title: '扫码成功',
content: `获取到的编码号:${result}`,
showCancel: false,
success: () => {
// +
this.isScanning = true;
this.resumeScan();
}
});
};
//
barcode.onerror = (error) => {
console.error('扫码错误:', error);
uni.showToast({ title: '扫码异常,请重试', icon: 'none' });
};
//
const currentWebview = plus.webview.currentWebview();
console.log('当前webview:', currentWebview);
console.log('扫码控件配置:', {
top: scanTop,
left: 15,
width: scanWidth,
height: scanHeight
});
currentWebview.append(barcode);
console.log('✅ 扫码控件已成功挂载到webview');
//
barcode.start((result) => {
console.log('扫码启动回调:', result);
if (result.code === 0) {
console.log('✅ 扫码已成功启动,相机画面应该显示');
this.barcodeInstance = barcode;
//
setTimeout(() => {
console.log('扫码控件状态检查:', {
exists: !!this.barcodeInstance,
isScanning: this.isScanning
});
}, 1000);
} else {
console.error('❌ 扫码启动失败:', result.message);
uni.showModal({
title: '扫码启动失败',
content: `错误信息: ${result.message || '未知错误'}\n请检查相机权限是否已开启`,
showCancel: false
});
}
});
console.log('扫码控件已挂载,正在启动...');
},
// H5
startScan() {
uni.scanCode({
success: (res) => {
uni.showModal({ title: '扫码成功', content: `获取到的编码号:${res.result}`, showCancel: false });
},
fail: () => uni.showToast({ title: '扫码失败', icon: 'none' })
});
},
// /
pauseScan() {
if (!this.barcodeInstance) return;
this.scanPaused = !this.scanPaused;
this.scanPaused ? this.barcodeInstance.pause() : this.barcodeInstance.resume();
uni.showToast({ title: this.scanPaused ? '扫码已暂停' : '扫码已继续', icon: 'none' });
},
//
resumeScan() {
if (this.barcodeInstance && this.scanPaused) {
this.barcodeInstance.resume();
this.scanPaused = false;
this.isScanning = true;
}
},
//
toggleFlash() {
if (!this.barcodeInstance) return;
this.flashOn = !this.flashOn;
this.barcodeInstance.setFlash(this.flashOn);
uni.showToast({ title: this.flashOn ? '手电已开启' : '手电已关闭', icon: 'none' });
},
// 使 uni.scanCode API
openFullscreenScan() {
console.log('打开全屏扫码');
//
if (this.barcodeInstance) {
this.barcodeInstance.pause();
}
// uniapp API
uni.scanCode({
scanType: ['barCode', 'qrCode'], //
onlyFromCamera: true, // 使
success: (res) => {
console.log('全屏扫码成功:', res);
//
uni.showModal({
title: '扫码成功',
content: `获取到的编码号:${res.result}`,
showCancel: false,
success: () => {
//
this.resumeScan();
}
});
},
fail: (err) => {
console.error('全屏扫码失败:', err);
//
this.resumeScan();
if (err.errMsg !== 'scanCode:fail cancel') {
uni.showToast({
title: '扫码失败,请重试',
icon: 'none'
});
}
}
});
}
}
};
</script>
<style scoped>
.input-list-page {
width: 100%;
height: 100vh;
background-color: #f5f5f5;
display: flex;
flex-direction: column;
}
.navbar {
width: 100%;
height: calc(var(--status-bar-height) + 44px);
background-color: #e60012;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 15px;
box-sizing: border-box;
position: fixed;
top: 0;
left: 0;
z-index: 999;
}
.nav-left, .nav-right {
display: flex;
align-items: center;
color: #fff;
}
.nav-title {
font-size: 18px;
font-weight: bold;
color: #fff;
}
.record-text {
font-size: 14px;
margin-left: 5px;
color: #fff;
}
.operation-bar {
margin-top: var(--status-bar-height) + 44px;
width: 100%;
display: flex;
align-items: center;
padding: 10px 15px;
box-sizing: border-box;
background-color: #fff;
gap: 10px;
}
.search-box {
flex: 1;
height: 32px;
border: 1px solid #eee;
border-radius: 4px;
display: flex;
align-items: center;
padding: 0 10px;
background-color: #f9f9f9;
}
.search-placeholder {
font-size: 14px;
color: #999;
}
.btn {
height: 32px;
padding: 0 12px;
font-size: 14px;
border-radius: 4px;
background-color: #f5f5f5;
border: 1px solid #eee;
color: #333;
display: flex;
align-items: center;
justify-content: center;
}
/* ✅ 修复:扫码区域样式优化,适配原生扫码图层 */
.scan-area {
width: 100%;
padding: 15px;
box-sizing: border-box;
position: relative;
min-height: 350px;
margin-top: 10px;
background-color: transparent;
}
.scan-tip {
position: absolute;
top: 15px;
left: 50%;
transform: translateX(-50%);
background-color: rgba(0,0,0,0.7);
color: #fff;
font-size: 14px;
padding: 4px 12px;
border-radius: 20px;
z-index: 10;
pointer-events: none;
}
.scan-view {
width: 100%;
height: 300px;
border-radius: 8px;
overflow: hidden;
position: relative;
background-color: #000;
border: 2px solid #e60012;
}
.scan-status {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 9;
pointer-events: none; /* 不遮挡扫码识别 */
}
.scan-line {
width: 80%;
height: 2px;
background-color: #e60012;
animation: scanMove 2s infinite;
}
@keyframes scanMove {
0% { transform: translateY(0); opacity: 0; }
50% { opacity: 1; }
100% { transform: translateY(250px); opacity: 0; }
}
.scan-status-text {
color: #fff;
font-size: 16px;
margin-top: 20px;
}
.scan-controls {
position: absolute;
bottom: 15px;
left: 0;
width: 100%;
display: flex;
justify-content: space-around;
padding: 0 15px;
box-sizing: border-box;
z-index: 10;
}
.control-item {
display: flex;
flex-direction: column;
align-items: center;
color: #fff;
cursor: pointer;
padding: 8px 12px;
border-radius: 8px;
background-color: rgba(0, 0, 0, 0.5);
transition: all 0.3s ease;
min-width: 60px;
}
.control-item:active {
background-color: rgba(230, 0, 18, 0.6);
transform: scale(0.95);
}
.control-text {
font-size: 12px;
margin-top: 5px;
text-align: center;
}
.h5-tip {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 20px;
background-color: #fff;
border-radius: 12px;
}
.tip-text {
font-size: 16px;
color: #333;
margin-bottom: 20px;
}
.scan-btn {
padding: 12px 30px;
background-color: #e60012;
color: #fff;
border-radius: 8px;
font-size: 16px;
}
.empty-area {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 20px;
box-sizing: border-box;
}
.empty-icon {
width: 80px;
height: 80px;
margin-bottom: 15px;
opacity: 0.5;
}
.empty-title {
font-size: 16px;
color: #333;
margin-bottom: 8px;
}
.empty-desc {
font-size: 14px;
color: #999;
margin-bottom: 10px;
}
</style>

1394
pages/index.vue Normal file

File diff suppressed because it is too large Load Diff

221
pages/login.vue Normal file
View File

@ -0,0 +1,221 @@
<template>
<view class="normal-login-container">
<view class="logo-content align-center justify-center flex">
<image style="width: 100rpx;height: 100rpx;" :src="globalConfig.appInfo.logo" mode="widthFix">
</image>
<text class="title">若依移动端登录</text>
</view>
<view class="login-form-content">
<view class="input-item flex align-center">
<view class="iconfont icon-user icon"></view>
<input v-model="loginForm.username" class="input" type="text" placeholder="请输入账号" maxlength="30" />
</view>
<view class="input-item flex align-center">
<view class="iconfont icon-password icon"></view>
<input v-model="loginForm.password" type="password" class="input" placeholder="请输入密码" maxlength="20" />
</view>
<view class="input-item flex align-center" style="width: 60%;margin: 0px;" v-if="captchaEnabled">
<view class="iconfont icon-code icon"></view>
<input v-model="loginForm.code" type="number" class="input" placeholder="请输入验证码" maxlength="4" />
<view class="login-code">
<image :src="codeUrl" @click="getCode" class="login-code-img"></image>
</view>
</view>
<view class="action-btn">
<button @click="handleLogin" class="login-btn cu-btn block bg-blue lg round">登录</button>
</view>
<view class="reg text-center" v-if="register">
<text class="text-grey1">没有账号</text>
<text @click="handleUserRegister" class="text-blue">立即注册</text>
</view>
<view class="xieyi text-center">
<text class="text-grey1">登录即代表同意</text>
<text @click="handleUserAgrement" class="text-blue">用户协议</text>
<text @click="handlePrivacy" class="text-blue">隐私协议</text>
</view>
</view>
</view>
</template>
<script>
import { getCodeImg } from '@/api/login'
import { getToken, setUserId } from '@/utils/auth'
export default {
data() {
return {
codeUrl: "",
captchaEnabled: true,
//
register: false,
globalConfig: getApp().globalData.config,
loginForm: {
username: "admin",
password: "123456",
code: "",
uuid: ""
}
}
},
created() {
this.getCode()
},
onLoad() {
//#ifdef H5
if (getToken()) {
this.$tab.reLaunch('/pages/index')
}
//#endif
},
methods: {
//
handleUserRegister() {
this.$tab.redirectTo(`/pages/register`)
},
//
handlePrivacy() {
let site = this.globalConfig.appInfo.agreements[0]
this.$tab.navigateTo(`/pages/common/webview/index?title=${site.title}&url=${site.url}`)
},
//
handleUserAgrement() {
let site = this.globalConfig.appInfo.agreements[1]
this.$tab.navigateTo(`/pages/common/webview/index?title=${site.title}&url=${site.url}`)
},
//
getCode() {
getCodeImg().then(res => {
this.captchaEnabled = res.captchaEnabled === undefined ? true : res.captchaEnabled
if (this.captchaEnabled) {
this.codeUrl = 'data:image/gif;base64,' + res.img
this.loginForm.uuid = res.uuid
}
})
},
//
async handleLogin() {
if (this.loginForm.username === "") {
this.$modal.msgError("请输入账号")
} else if (this.loginForm.password === "") {
this.$modal.msgError("请输入密码")
} else if (this.loginForm.code === "" && this.captchaEnabled) {
this.$modal.msgError("请输入验证码")
} else {
this.$modal.loading("登录中,请耐心等待...")
this.pwdLogin()
}
},
//
async pwdLogin() {
this.$store.dispatch('Login', this.loginForm).then(() => {
this.$modal.closeLoading()
this.loginSuccess()
}).catch(() => {
if (this.captchaEnabled) {
this.getCode()
}
})
},
//
loginSuccess(result) {
// ID
this.$store.dispatch('GetInfo').then(res => {
console.log('用户信息:', res);
// ID
if (res.user && res.user.userId) {
setUserId(res.user.userId);
console.log('用户ID已存储:', res.user.userId);
}
//
uni.redirectTo({
url: '/pages/storeSelect/storeSelect'
})
})
}
}
}
</script>
<style lang="scss" scoped>
page {
background-color: #ffffff;
}
.normal-login-container {
width: 100%;
.logo-content {
width: 100%;
font-size: 21px;
text-align: center;
padding-top: 15%;
image {
border-radius: 4px;
}
.title {
margin-left: 10px;
}
}
.login-form-content {
text-align: center;
margin: 20px auto;
margin-top: 15%;
width: 80%;
.input-item {
margin: 20px auto;
background-color: #f5f6f7;
height: 45px;
border-radius: 20px;
.icon {
font-size: 38rpx;
margin-left: 10px;
color: #999;
}
.input {
width: 100%;
font-size: 14px;
line-height: 20px;
text-align: left;
padding-left: 15px;
}
}
.login-btn {
margin-top: 40px;
height: 45px;
}
.reg {
margin-top: 15px;
}
.xieyi {
color: #333;
margin-top: 20px;
}
.login-code {
height: 38px;
float: right;
.login-code-img {
height: 38px;
position: absolute;
margin-left: 10px;
width: 200rpx;
}
}
}
}
</style>

109
pages/menu.vue Normal file
View File

@ -0,0 +1,109 @@
<template>
<view class="menu">
<!-- 顶部用户信息 -->
<view class="user-info">
<image class="avatar" src="/static/avatar.png"></image>
<text class="name">个人店</text>
<text class="sub">商店智能店</text>
<text class="stats">已上线50万会员 | 管理员</text>
</view>
<!-- 企业管理 -->
<view class="section">
<text class="section-title">企业管理</text>
<view class="section-item">
<text class="item-label">企业名称</text>
<text class="item-value">总部/管理员</text>
</view>
<view class="section-item">
<text class="item-label">创建企业组织</text>
<text class="item-label">创建店铺</text>
</view>
</view>
<!-- 其他菜单项 -->
<view class="menu-list">
<view class="menu-item" @click="goToPage('shop')">
<text></text>
<text>202 今日进店</text>
</view>
<view class="menu-item" @click="goToPage('product')">
<text>商品管理</text>
</view>
<!-- 其他菜单项... -->
</view>
</view>
</template>
<script>
export default {
methods: {
goToPage(page) {
uni.navigateTo({ url: `/pages/${page}/${page}` })
this.$emit('close')
}
}
}
</script>
<style scoped>
.menu {
padding: 40rpx;
height: 100%;
background: #fff;
}
.user-info {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 40rpx;
}
.avatar {
width: 120rpx;
height: 120rpx;
border-radius: 50%;
margin-bottom: 20rpx;
}
.name {
font-size: 36rpx;
font-weight: bold;
}
.sub {
font-size: 28rpx;
color: #666;
margin: 10rpx 0;
}
.stats {
font-size: 24rpx;
color: #888;
}
.section {
margin: 30rpx 0;
}
.section-title {
font-size: 30rpx;
font-weight: bold;
display: block;
margin-bottom: 20rpx;
}
.section-item {
display: flex;
justify-content: space-between;
margin: 15rpx 0;
}
.menu-item {
padding: 25rpx 0;
border-bottom: 1rpx solid #eee;
display: flex;
justify-content: space-between;
}
</style>

View File

@ -0,0 +1,75 @@
<template>
<view class="about-container">
<view class="header-section text-center">
<image style="width: 150rpx;height: 150rpx;" src="/static/logo200.png" mode="widthFix">
</image>
<uni-title type="h2" title="若依移动端"></uni-title>
</view>
<view class="content-section">
<view class="menu-list">
<view class="list-cell list-cell-arrow">
<view class="menu-item-box">
<view>版本信息</view>
<view class="text-right">v{{version}}</view>
</view>
</view>
<view class="list-cell list-cell-arrow">
<view class="menu-item-box">
<view>官方邮箱</view>
<view class="text-right">ruoyi@xx.com</view>
</view>
</view>
<view class="list-cell list-cell-arrow">
<view class="menu-item-box">
<view>服务热线</view>
<view class="text-right">400-999-9999</view>
</view>
</view>
<view class="list-cell list-cell-arrow">
<view class="menu-item-box">
<view>公司网站</view>
<view class="text-right">
<uni-link :href="url" :text="url" showUnderLine="false"></uni-link>
</view>
</view>
</view>
</view>
</view>
<view class="copyright">
<view>Copyright &copy; 2025 ruoyi.vip All Rights Reserved.</view>
</view>
</view>
</template>
<script>
export default {
data() {
return {
url: getApp().globalData.config.appInfo.site_url,
version: getApp().globalData.config.appInfo.version
}
}
}
</script>
<style lang="scss" scoped>
page {
background-color: #f8f8f8;
}
.copyright {
margin-top: 50rpx;
text-align: center;
line-height: 60rpx;
color: #999;
}
.header-section {
display: flex;
padding: 30rpx 0 0;
flex-direction: column;
align-items: center;
}
</style>

618
pages/mine/avatar/index.vue Normal file
View File

@ -0,0 +1,618 @@
<template>
<view class="container">
<view class="page-body uni-content-info">
<view class='cropper-content'>
<view v-if="isShowImg" class="uni-corpper" :style="'width:'+cropperInitW+'px;height:'+cropperInitH+'px;background:#000'">
<view class="uni-corpper-content" :style="'width:'+cropperW+'px;height:'+cropperH+'px;left:'+cropperL+'px;top:'+cropperT+'px'">
<image :src="imageSrc" :style="'width:'+cropperW+'px;height:'+cropperH+'px'"></image>
<view class="uni-corpper-crop-box" @touchstart.stop="contentStartMove" @touchmove.stop="contentMoveing" @touchend.stop="contentTouchEnd"
:style="'left:'+cutL+'px;top:'+cutT+'px;right:'+cutR+'px;bottom:'+cutB+'px'">
<view class="uni-cropper-view-box">
<view class="uni-cropper-dashed-h"></view>
<view class="uni-cropper-dashed-v"></view>
<view class="uni-cropper-line-t" data-drag="top" @touchstart.stop="dragStart" @touchmove.stop="dragMove"></view>
<view class="uni-cropper-line-r" data-drag="right" @touchstart.stop="dragStart" @touchmove.stop="dragMove"></view>
<view class="uni-cropper-line-b" data-drag="bottom" @touchstart.stop="dragStart" @touchmove.stop="dragMove"></view>
<view class="uni-cropper-line-l" data-drag="left" @touchstart.stop="dragStart" @touchmove.stop="dragMove"></view>
<view class="uni-cropper-point point-t" data-drag="top" @touchstart.stop="dragStart" @touchmove.stop="dragMove"></view>
<view class="uni-cropper-point point-tr" data-drag="topTight"></view>
<view class="uni-cropper-point point-r" data-drag="right" @touchstart.stop="dragStart" @touchmove.stop="dragMove"></view>
<view class="uni-cropper-point point-rb" data-drag="rightBottom" @touchstart.stop="dragStart" @touchmove.stop="dragMove"></view>
<view class="uni-cropper-point point-b" data-drag="bottom" @touchstart.stop="dragStart" @touchmove.stop="dragMove"></view>
<view class="uni-cropper-point point-bl" data-drag="bottomLeft"></view>
<view class="uni-cropper-point point-l" data-drag="left" @touchstart.stop="dragStart" @touchmove.stop="dragMove"></view>
<view class="uni-cropper-point point-lt" data-drag="leftTop"></view>
</view>
</view>
</view>
</view>
</view>
<view class='cropper-config'>
<button type="primary reverse" @click="getImage" style='margin-top: 30rpx;'> 选择头像 </button>
<button type="warn" @click="getImageInfo" style='margin-top: 30rpx;'> 提交 </button>
</view>
<canvas canvas-id="myCanvas" :style="'position:absolute;border: 1px solid red; width:'+imageW+'px;height:'+imageH+'px;top:-9999px;left:-9999px;'"></canvas>
</view>
</view>
</template>
<script>
import config from '@/config'
import store from "@/store"
import { uploadAvatar } from "@/api/system/user"
const baseUrl = config.baseUrl
let sysInfo = uni.getSystemInfoSync()
let SCREEN_WIDTH = sysInfo.screenWidth
let PAGE_X, // x
PAGE_Y, // y
PR = sysInfo.pixelRatio, // dpi
T_PAGE_X, // x
T_PAGE_Y, // Y
CUT_L, // left
CUT_T, // top
CUT_R, //
CUT_B, //
CUT_W, //
CUT_H, //
IMG_RATIO, //
IMG_REAL_W, //
IMG_REAL_H, //
DRAFG_MOVE_RATIO = 1, //,
INIT_DRAG_POSITION = 100, //
DRAW_IMAGE_W = sysInfo.screenWidth //
export default {
/**
* 页面的初始数据
*/
data() {
return {
imageSrc: store.getters.avatar,
isShowImg: false,
//
cropperInitW: SCREEN_WIDTH,
cropperInitH: SCREEN_WIDTH,
//
cropperW: SCREEN_WIDTH,
cropperH: SCREEN_WIDTH,
// left top
cropperL: 0,
cropperT: 0,
transL: 0,
transT: 0,
//
scaleP: 0,
imageW: 0,
imageH: 0,
//
cutL: 0,
cutT: 0,
cutB: SCREEN_WIDTH,
cutR: '100%',
qualityWidth: DRAW_IMAGE_W,
innerAspectRadio: DRAFG_MOVE_RATIO
}
},
/**
* 生命周期函数--监听页面初次渲染完成
*/
onReady: function () {
this.loadImage()
},
methods: {
setData: function (obj) {
let that = this
Object.keys(obj).forEach(function (key) {
that.$set(that.$data, key, obj[key])
})
},
getImage: function () {
var _this = this
uni.chooseImage({
success: function (res) {
_this.setData({
imageSrc: res.tempFilePaths[0],
})
_this.loadImage()
},
})
},
loadImage: function () {
var _this = this
uni.getImageInfo({
src: _this.imageSrc,
success: function success(res) {
IMG_RATIO = 1 / 1
if (IMG_RATIO >= 1) {
IMG_REAL_W = SCREEN_WIDTH
IMG_REAL_H = SCREEN_WIDTH / IMG_RATIO
} else {
IMG_REAL_W = SCREEN_WIDTH * IMG_RATIO
IMG_REAL_H = SCREEN_WIDTH
}
let minRange = IMG_REAL_W > IMG_REAL_H ? IMG_REAL_W : IMG_REAL_H
INIT_DRAG_POSITION = minRange > INIT_DRAG_POSITION ? INIT_DRAG_POSITION : minRange
//
if (IMG_RATIO >= 1) {
let cutT = Math.ceil((SCREEN_WIDTH / IMG_RATIO - (SCREEN_WIDTH / IMG_RATIO - INIT_DRAG_POSITION)) / 2)
let cutB = cutT
let cutL = Math.ceil((SCREEN_WIDTH - SCREEN_WIDTH + INIT_DRAG_POSITION) / 2)
let cutR = cutL
_this.setData({
cropperW: SCREEN_WIDTH,
cropperH: SCREEN_WIDTH / IMG_RATIO,
// left right
cropperL: Math.ceil((SCREEN_WIDTH - SCREEN_WIDTH) / 2),
cropperT: Math.ceil((SCREEN_WIDTH - SCREEN_WIDTH / IMG_RATIO) / 2),
cutL: cutL,
cutT: cutT,
cutR: cutR,
cutB: cutB,
//
imageW: IMG_REAL_W,
imageH: IMG_REAL_H,
scaleP: IMG_REAL_W / SCREEN_WIDTH,
qualityWidth: DRAW_IMAGE_W,
innerAspectRadio: IMG_RATIO
})
} else {
let cutL = Math.ceil((SCREEN_WIDTH * IMG_RATIO - (SCREEN_WIDTH * IMG_RATIO)) / 2)
let cutR = cutL
let cutT = Math.ceil((SCREEN_WIDTH - INIT_DRAG_POSITION) / 2)
let cutB = cutT
_this.setData({
cropperW: SCREEN_WIDTH * IMG_RATIO,
cropperH: SCREEN_WIDTH,
// left right
cropperL: Math.ceil((SCREEN_WIDTH - SCREEN_WIDTH * IMG_RATIO) / 2),
cropperT: Math.ceil((SCREEN_WIDTH - SCREEN_WIDTH) / 2),
cutL: cutL,
cutT: cutT,
cutR: cutR,
cutB: cutB,
//
imageW: IMG_REAL_W,
imageH: IMG_REAL_H,
scaleP: IMG_REAL_W / SCREEN_WIDTH,
qualityWidth: DRAW_IMAGE_W,
innerAspectRadio: IMG_RATIO
})
}
_this.setData({
isShowImg: true
})
uni.hideLoading()
}
})
},
// touchStart
contentStartMove(e) {
PAGE_X = e.touches[0].pageX
PAGE_Y = e.touches[0].pageY
},
// touchMove
contentMoveing(e) {
var _this = this
var dragLengthX = (PAGE_X - e.touches[0].pageX) * DRAFG_MOVE_RATIO
var dragLengthY = (PAGE_Y - e.touches[0].pageY) * DRAFG_MOVE_RATIO
//
if (dragLengthX > 0) {
if (this.cutL - dragLengthX < 0) dragLengthX = this.cutL
} else {
if (this.cutR + dragLengthX < 0) dragLengthX = -this.cutR
}
if (dragLengthY > 0) {
if (this.cutT - dragLengthY < 0) dragLengthY = this.cutT
} else {
if (this.cutB + dragLengthY < 0) dragLengthY = -this.cutB
}
this.setData({
cutL: this.cutL - dragLengthX,
cutT: this.cutT - dragLengthY,
cutR: this.cutR + dragLengthX,
cutB: this.cutB + dragLengthY
})
PAGE_X = e.touches[0].pageX
PAGE_Y = e.touches[0].pageY
},
contentTouchEnd() {
},
//
getImageInfo() {
var _this = this
uni.showLoading({
title: '图片生成中...',
})
//
const ctx = uni.createCanvasContext('myCanvas')
ctx.drawImage(_this.imageSrc, 0, 0, IMG_REAL_W, IMG_REAL_H)
ctx.draw(true, () => {
// * canvasT = (_this.cutT / _this.cropperH) * (_this.imageH / pixelRatio)
var canvasW = ((_this.cropperW - _this.cutL - _this.cutR) / _this.cropperW) * IMG_REAL_W
var canvasH = ((_this.cropperH - _this.cutT - _this.cutB) / _this.cropperH) * IMG_REAL_H
var canvasL = (_this.cutL / _this.cropperW) * IMG_REAL_W
var canvasT = (_this.cutT / _this.cropperH) * IMG_REAL_H
uni.canvasToTempFilePath({
x: canvasL,
y: canvasT,
width: canvasW,
height: canvasH,
destWidth: canvasW,
destHeight: canvasH,
quality: 0.5,
canvasId: 'myCanvas',
success: function (res) {
uni.hideLoading()
let data = {name: 'avatarfile', filePath: res.tempFilePath}
uploadAvatar(data).then(response => {
store.commit('SET_AVATAR', baseUrl + response.imgUrl)
uni.showToast({ title: "修改成功", icon: 'success' })
uni.navigateBack()
})
}
})
})
},
// touchStart
dragStart(e) {
T_PAGE_X = e.touches[0].pageX
T_PAGE_Y = e.touches[0].pageY
CUT_L = this.cutL
CUT_R = this.cutR
CUT_B = this.cutB
CUT_T = this.cutT
},
// touchMove
dragMove(e) {
var _this = this
var dragType = e.target.dataset.drag
switch (dragType) {
case 'right':
var dragLength = (T_PAGE_X - e.touches[0].pageX) * DRAFG_MOVE_RATIO
if (CUT_R + dragLength < 0) dragLength = -CUT_R
this.setData({
cutR: CUT_R + dragLength
})
break
case 'left':
var dragLength = (T_PAGE_X - e.touches[0].pageX) * DRAFG_MOVE_RATIO
if (CUT_L - dragLength < 0) dragLength = CUT_L
if ((CUT_L - dragLength) > (this.cropperW - this.cutR)) dragLength = CUT_L - (this.cropperW - this.cutR)
this.setData({
cutL: CUT_L - dragLength
})
break
case 'top':
var dragLength = (T_PAGE_Y - e.touches[0].pageY) * DRAFG_MOVE_RATIO
if (CUT_T - dragLength < 0) dragLength = CUT_T
if ((CUT_T - dragLength) > (this.cropperH - this.cutB)) dragLength = CUT_T - (this.cropperH - this.cutB)
this.setData({
cutT: CUT_T - dragLength
})
break
case 'bottom':
var dragLength = (T_PAGE_Y - e.touches[0].pageY) * DRAFG_MOVE_RATIO
if (CUT_B + dragLength < 0) dragLength = -CUT_B
this.setData({
cutB: CUT_B + dragLength
})
break
case 'rightBottom':
var dragLengthX = (T_PAGE_X - e.touches[0].pageX) * DRAFG_MOVE_RATIO
var dragLengthY = (T_PAGE_Y - e.touches[0].pageY) * DRAFG_MOVE_RATIO
if (CUT_B + dragLengthY < 0) dragLengthY = -CUT_B
if (CUT_R + dragLengthX < 0) dragLengthX = -CUT_R
let cutB = CUT_B + dragLengthY
let cutR = CUT_R + dragLengthX
this.setData({
cutB: cutB,
cutR: cutR
})
break
default:
break
}
}
}
}
</script>
<style scoped>
.cropper-config {
padding: 20rpx 40rpx;
}
.cropper-content {
min-height: 750rpx;
width: 100%;
}
.uni-corpper {
position: relative;
overflow: hidden;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
-webkit-tap-highlight-color: transparent;
-webkit-touch-callout: none;
box-sizing: border-box;
}
.uni-corpper-content {
position: relative;
}
.uni-corpper-content image {
display: block;
width: 100%;
min-width: 0 !important;
max-width: none !important;
height: 100%;
min-height: 0 !important;
max-height: none !important;
image-orientation: 0deg !important;
margin: 0 auto;
}
/* 移动图片效果 */
.uni-cropper-drag-box {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
cursor: move;
background: rgba(0, 0, 0, 0.6);
z-index: 1;
}
/* 内部的信息 */
.uni-corpper-crop-box {
position: absolute;
background: rgba(255, 255, 255, 0.3);
z-index: 2;
}
.uni-corpper-crop-box .uni-cropper-view-box {
position: relative;
display: block;
width: 100%;
height: 100%;
overflow: visible;
outline: 1rpx solid #69f;
outline-color: rgba(102, 153, 255, .75)
}
/* 横向虚线 */
.uni-cropper-dashed-h {
position: absolute;
top: 33.33333333%;
left: 0;
width: 100%;
height: 33.33333333%;
border-top: 1rpx dashed rgba(255, 255, 255, 0.5);
border-bottom: 1rpx dashed rgba(255, 255, 255, 0.5);
}
/* 纵向虚线 */
.uni-cropper-dashed-v {
position: absolute;
left: 33.33333333%;
top: 0;
width: 33.33333333%;
height: 100%;
border-left: 1rpx dashed rgba(255, 255, 255, 0.5);
border-right: 1rpx dashed rgba(255, 255, 255, 0.5);
}
/* 四个方向的线 为了之后的拖动事件*/
.uni-cropper-line-t {
position: absolute;
display: block;
width: 100%;
background-color: #69f;
top: 0;
left: 0;
height: 1rpx;
opacity: 0.1;
cursor: n-resize;
}
.uni-cropper-line-t::before {
content: '';
position: absolute;
top: 50%;
right: 0rpx;
width: 100%;
-webkit-transform: translate3d(0, -50%, 0);
transform: translate3d(0, -50%, 0);
bottom: 0;
height: 41rpx;
background: transparent;
z-index: 11;
}
.uni-cropper-line-r {
position: absolute;
display: block;
background-color: #69f;
top: 0;
right: 0rpx;
width: 1rpx;
opacity: 0.1;
height: 100%;
cursor: e-resize;
}
.uni-cropper-line-r::before {
content: '';
position: absolute;
top: 0;
left: 50%;
width: 41rpx;
-webkit-transform: translate3d(-50%, 0, 0);
transform: translate3d(-50%, 0, 0);
bottom: 0;
height: 100%;
background: transparent;
z-index: 11;
}
.uni-cropper-line-b {
position: absolute;
display: block;
width: 100%;
background-color: #69f;
bottom: 0;
left: 0;
height: 1rpx;
opacity: 0.1;
cursor: s-resize;
}
.uni-cropper-line-b::before {
content: '';
position: absolute;
top: 50%;
right: 0rpx;
width: 100%;
-webkit-transform: translate3d(0, -50%, 0);
transform: translate3d(0, -50%, 0);
bottom: 0;
height: 41rpx;
background: transparent;
z-index: 11;
}
.uni-cropper-line-l {
position: absolute;
display: block;
background-color: #69f;
top: 0;
left: 0;
width: 1rpx;
opacity: 0.1;
height: 100%;
cursor: w-resize;
}
.uni-cropper-line-l::before {
content: '';
position: absolute;
top: 0;
left: 50%;
width: 41rpx;
-webkit-transform: translate3d(-50%, 0, 0);
transform: translate3d(-50%, 0, 0);
bottom: 0;
height: 100%;
background: transparent;
z-index: 11;
}
.uni-cropper-point {
width: 5rpx;
height: 5rpx;
background-color: #69f;
opacity: .75;
position: absolute;
z-index: 3;
}
.point-t {
top: -3rpx;
left: 50%;
margin-left: -3rpx;
cursor: n-resize;
}
.point-tr {
top: -3rpx;
left: 100%;
margin-left: -3rpx;
cursor: n-resize;
}
.point-r {
top: 50%;
left: 100%;
margin-left: -3rpx;
margin-top: -3rpx;
cursor: n-resize;
}
.point-rb {
left: 100%;
top: 100%;
-webkit-transform: translate3d(-50%, -50%, 0);
transform: translate3d(-50%, -50%, 0);
cursor: n-resize;
width: 36rpx;
height: 36rpx;
background-color: #69f;
position: absolute;
z-index: 1112;
opacity: 1;
}
.point-b {
left: 50%;
top: 100%;
margin-left: -3rpx;
margin-top: -3rpx;
cursor: n-resize;
}
.point-bl {
left: 0%;
top: 100%;
margin-left: -3rpx;
margin-top: -3rpx;
cursor: n-resize;
}
.point-l {
left: 0%;
top: 50%;
margin-left: -3rpx;
margin-top: -3rpx;
cursor: n-resize;
}
.point-lt {
left: 0%;
top: 0%;
margin-left: -3rpx;
margin-top: -3rpx;
cursor: n-resize;
}
/* 裁剪框预览内容 */
.uni-cropper-viewer {
position: relative;
width: 100%;
height: 100%;
overflow: hidden;
}
.uni-cropper-viewer image {
position: absolute;
z-index: 2;
}
</style>

112
pages/mine/help/index.vue Normal file
View File

@ -0,0 +1,112 @@
<template>
<view class="help-container">
<view v-for="(item, findex) in list" :key="findex" :title="item.title" class="list-title">
<view class="text-title">
<view :class="item.icon"></view>{{ item.title }}
</view>
<view class="childList">
<view v-for="(child, zindex) in item.childList" :key="zindex" class="question" hover-class="hover"
@click="handleText(child)">
<view class="text-item">{{ child.title }}</view>
<view class="line" v-if="zindex !== item.childList.length - 1"></view>
</view>
</view>
</view>
</view>
</template>
<script>
export default {
data() {
return {
list: [{
icon: 'iconfont icon-github',
title: '若依问题',
childList: [{
title: '若依开源吗?',
content: '开源'
}, {
title: '若依可以商用吗?',
content: '可以'
}, {
title: '若依官网地址多少?',
content: 'http://ruoyi.vip'
}, {
title: '若依文档地址多少?',
content: 'http://doc.ruoyi.vip'
}]
},
{
icon: 'iconfont icon-help',
title: '其他问题',
childList: [{
title: '如何退出登录?',
content: '请点击[我的] - [应用设置] - [退出登录]即可退出登录',
}, {
title: '如何修改用户头像?',
content: '请点击[我的] - [选择头像] - [点击提交]即可更换用户头像',
}, {
title: '如何修改登录密码?',
content: '请点击[我的] - [应用设置] - [修改密码]即可修改登录密码',
}]
}
]
}
},
methods: {
handleText(item) {
this.$tab.navigateTo(`/pages/common/textview/index?title=${item.title}&content=${item.content}`)
}
}
}
</script>
<style lang="scss" scoped>
page {
background-color: #f8f8f8;
}
.help-container {
margin-bottom: 100rpx;
padding: 30rpx;
}
.list-title {
margin-bottom: 30rpx;
}
.childList {
background: #ffffff;
box-shadow: 0px 0px 10rpx rgba(193, 193, 193, 0.2);
border-radius: 16rpx;
margin-top: 10rpx;
}
.line {
width: 100%;
height: 1rpx;
background-color: #F5F5F5;
}
.text-title {
color: #303133;
font-size: 32rpx;
font-weight: bold;
margin-left: 10rpx;
.iconfont {
font-size: 16px;
margin-right: 10rpx;
}
}
.text-item {
font-size: 28rpx;
padding: 24rpx;
}
.question {
color: #606266;
font-size: 28rpx;
}
</style>

188
pages/mine/index.vue Normal file
View File

@ -0,0 +1,188 @@
<template>
<view class="mine-container" :style="{height: `${windowHeight}px`}">
<!--顶部个人信息栏-->
<view class="header-section">
<view class="flex padding justify-between">
<view class="flex align-center">
<view v-if="!avatar" class="cu-avatar xl round bg-white">
<view class="iconfont icon-people text-gray icon"></view>
</view>
<image v-if="avatar" @click="handleToAvatar" :src="avatar" class="cu-avatar xl round" mode="widthFix">
</image>
<view v-if="!name" @click="handleToLogin" class="login-tip">
点击登录
</view>
<view v-if="name" @click="handleToInfo" class="user-info">
<view class="u_title">
用户名{{ name }}
</view>
</view>
</view>
<view @click="handleToInfo" class="flex align-center">
<text>个人信息</text>
<view class="iconfont icon-right"></view>
</view>
</view>
</view>
<view class="content-section">
<view class="mine-actions grid col-4 text-center">
<view class="action-item" @click="handleJiaoLiuQun">
<view class="iconfont icon-friendfill text-pink icon"></view>
<text class="text">交流群</text>
</view>
<view class="action-item" @click="handleBuilding">
<view class="iconfont icon-service text-blue icon"></view>
<text class="text">在线客服</text>
</view>
<view class="action-item" @click="handleBuilding">
<view class="iconfont icon-community text-mauve icon"></view>
<text class="text">反馈社区</text>
</view>
<view class="action-item" @click="handleBuilding">
<view class="iconfont icon-dianzan text-green icon"></view>
<text class="text">点赞我们</text>
</view>
</view>
<view class="menu-list">
<view class="list-cell list-cell-arrow" @click="handleToEditInfo">
<view class="menu-item-box">
<view class="iconfont icon-user menu-icon"></view>
<view>编辑资料</view>
</view>
</view>
<view class="list-cell list-cell-arrow" @click="handleHelp">
<view class="menu-item-box">
<view class="iconfont icon-help menu-icon"></view>
<view>常见问题</view>
</view>
</view>
<view class="list-cell list-cell-arrow" @click="handleAbout">
<view class="menu-item-box">
<view class="iconfont icon-aixin menu-icon"></view>
<view>关于我们</view>
</view>
</view>
<view class="list-cell list-cell-arrow" @click="handleToSetting">
<view class="menu-item-box">
<view class="iconfont icon-setting menu-icon"></view>
<view>应用设置</view>
</view>
</view>
</view>
</view>
</view>
</template>
<script>
export default {
data() {
return {
name: this.$store.state.user.name
}
},
computed: {
avatar() {
return this.$store.state.user.avatar
},
windowHeight() {
return uni.getSystemInfoSync().windowHeight - 50
}
},
methods: {
handleToInfo() {
this.$tab.navigateTo('/pages/mine/info/index')
},
handleToEditInfo() {
this.$tab.navigateTo('/pages/mine/info/edit')
},
handleToSetting() {
this.$tab.navigateTo('/pages/mine/setting/index')
},
handleToLogin() {
this.$tab.reLaunch('/pages/login')
},
handleToAvatar() {
this.$tab.navigateTo('/pages/mine/avatar/index')
},
handleHelp() {
this.$tab.navigateTo('/pages/mine/help/index')
},
handleAbout() {
this.$tab.navigateTo('/pages/mine/about/index')
},
handleJiaoLiuQun() {
this.$modal.showToast('QQ群①133713780(满)、②146013835(满)、③189091635')
},
handleBuilding() {
this.$modal.showToast('模块建设中~')
}
}
}
</script>
<style lang="scss" scoped>
page {
background-color: #f5f6f7;
}
.mine-container {
width: 100%;
height: 100%;
.header-section {
padding: 15px 15px 45px 15px;
background-color: #3c96f3;
color: white;
.login-tip {
font-size: 18px;
margin-left: 10px;
}
.cu-avatar {
border: 2px solid #eaeaea;
.icon {
font-size: 40px;
}
}
.user-info {
margin-left: 15px;
.u_title {
font-size: 18px;
line-height: 30px;
}
}
}
.content-section {
position: relative;
top: -50px;
.mine-actions {
margin: 15px 15px;
padding: 20px 0px;
border-radius: 8px;
background-color: white;
.action-item {
.icon {
font-size: 28px;
}
.text {
display: block;
font-size: 13px;
margin: 8px 0px;
}
}
}
}
}
</style>

127
pages/mine/info/edit.vue Normal file
View File

@ -0,0 +1,127 @@
<template>
<view class="container">
<view class="example">
<uni-forms ref="form" :model="user" labelWidth="80px">
<uni-forms-item label="用户昵称" name="nickName">
<uni-easyinput v-model="user.nickName" placeholder="请输入昵称" />
</uni-forms-item>
<uni-forms-item label="手机号码" name="phonenumber">
<uni-easyinput v-model="user.phonenumber" placeholder="请输入手机号码" />
</uni-forms-item>
<uni-forms-item label="邮箱" name="email">
<uni-easyinput v-model="user.email" placeholder="请输入邮箱" />
</uni-forms-item>
<uni-forms-item label="性别" name="sex" required>
<uni-data-checkbox v-model="user.sex" :localdata="sexs" />
</uni-forms-item>
</uni-forms>
<button type="primary" @click="submit"></button>
</view>
</view>
</template>
<script>
import { getUserProfile } from "@/api/system/user"
import { updateUserProfile } from "@/api/system/user"
export default {
data() {
return {
user: {
nickName: "",
phonenumber: "",
email: "",
sex: ""
},
sexs: [{
text: '男',
value: "0"
}, {
text: '女',
value: "1"
}],
rules: {
nickName: {
rules: [{
required: true,
errorMessage: '用户昵称不能为空'
}]
},
phonenumber: {
rules: [{
required: true,
errorMessage: '手机号码不能为空'
}, {
pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/,
errorMessage: '请输入正确的手机号码'
}]
},
email: {
rules: [{
required: true,
errorMessage: '邮箱地址不能为空'
}, {
format: 'email',
errorMessage: '请输入正确的邮箱地址'
}]
}
}
}
},
onLoad() {
this.getUser()
},
onReady() {
this.$refs.form.setRules(this.rules)
},
methods: {
getUser() {
getUserProfile().then(response => {
this.user = response.data
})
},
submit(ref) {
this.$refs.form.validate().then(res => {
updateUserProfile(this.user).then(response => {
this.$modal.msgSuccess("修改成功")
})
})
}
}
}
</script>
<style lang="scss" scoped>
page {
background-color: #ffffff;
}
.example {
padding: 15px;
background-color: #fff;
}
.segmented-control {
margin-bottom: 15px;
}
.button-group {
margin-top: 15px;
display: flex;
justify-content: space-around;
}
.form-item {
display: flex;
align-items: center;
flex: 1;
}
.button {
display: flex;
align-items: center;
height: 35px;
line-height: 35px;
margin-left: 10px;
}
</style>

44
pages/mine/info/index.vue Normal file
View File

@ -0,0 +1,44 @@
<template>
<view class="container">
<uni-list>
<uni-list-item showExtraIcon="true" :extraIcon="{type: 'person-filled'}" title="昵称" :rightText="user.nickName" />
<uni-list-item showExtraIcon="true" :extraIcon="{type: 'phone-filled'}" title="手机号码" :rightText="user.phonenumber" />
<uni-list-item showExtraIcon="true" :extraIcon="{type: 'email-filled'}" title="邮箱" :rightText="user.email" />
<uni-list-item showExtraIcon="true" :extraIcon="{type: 'auth-filled'}" title="岗位" :rightText="postGroup" />
<uni-list-item showExtraIcon="true" :extraIcon="{type: 'staff-filled'}" title="角色" :rightText="roleGroup" />
<uni-list-item showExtraIcon="true" :extraIcon="{type: 'calendar-filled'}" title="创建日期" :rightText="user.createTime" />
</uni-list>
</view>
</template>
<script>
import { getUserProfile } from "@/api/system/user"
export default {
data() {
return {
user: {},
roleGroup: "",
postGroup: ""
}
},
onLoad() {
this.getUser()
},
methods: {
getUser() {
getUserProfile().then(response => {
this.user = response.data
this.roleGroup = response.roleGroup
this.postGroup = response.postGroup
})
}
}
}
</script>
<style lang="scss">
page {
background-color: #ffffff;
}
</style>

85
pages/mine/pwd/index.vue Normal file
View File

@ -0,0 +1,85 @@
<template>
<view class="pwd-retrieve-container">
<uni-forms ref="form" :value="user" labelWidth="80px">
<uni-forms-item name="oldPassword" label="旧密码">
<uni-easyinput type="password" v-model="user.oldPassword" placeholder="请输入旧密码" />
</uni-forms-item>
<uni-forms-item name="newPassword" label="新密码">
<uni-easyinput type="password" v-model="user.newPassword" placeholder="请输入新密码" />
</uni-forms-item>
<uni-forms-item name="confirmPassword" label="确认密码">
<uni-easyinput type="password" v-model="user.confirmPassword" placeholder="请确认新密码" />
</uni-forms-item>
<button type="primary" @click="submit"></button>
</uni-forms>
</view>
</template>
<script>
import { updateUserPwd } from "@/api/system/user"
export default {
data() {
return {
user: {
oldPassword: undefined,
newPassword: undefined,
confirmPassword: undefined
},
rules: {
oldPassword: {
rules: [{
required: true,
errorMessage: '旧密码不能为空'
}]
},
newPassword: {
rules: [{
required: true,
errorMessage: '新密码不能为空',
},
{
minLength: 6,
maxLength: 20,
errorMessage: '长度在 6 到 20 个字符'
}
]
},
confirmPassword: {
rules: [{
required: true,
errorMessage: '确认密码不能为空'
}, {
validateFunction: (rule, value, data) => data.newPassword === value,
errorMessage: '两次输入的密码不一致'
}
]
}
}
}
},
onReady() {
this.$refs.form.setRules(this.rules)
},
methods: {
submit() {
this.$refs.form.validate().then(res => {
updateUserPwd(this.user.oldPassword, this.user.newPassword).then(response => {
this.$modal.msgSuccess("修改成功")
})
})
}
}
}
</script>
<style lang="scss" scoped>
page {
background-color: #ffffff;
}
.pwd-retrieve-container {
padding-top: 36rpx;
padding: 15px;
}
</style>

View File

@ -0,0 +1,78 @@
<template>
<view class="setting-container" :style="{height: `${windowHeight}px`}">
<view class="menu-list">
<view class="list-cell list-cell-arrow" @click="handleToPwd">
<view class="menu-item-box">
<view class="iconfont icon-password menu-icon"></view>
<view>修改密码</view>
</view>
</view>
<view class="list-cell list-cell-arrow" @click="handleToUpgrade">
<view class="menu-item-box">
<view class="iconfont icon-refresh menu-icon"></view>
<view>检查更新</view>
</view>
</view>
<view class="list-cell list-cell-arrow" @click="handleCleanTmp">
<view class="menu-item-box">
<view class="iconfont icon-clean menu-icon"></view>
<view>清理缓存</view>
</view>
</view>
</view>
<view class="cu-list menu">
<view class="cu-item item-box">
<view class="content text-center" @click="handleLogout">
<text class="text-black">退出登录</text>
</view>
</view>
</view>
</view>
</template>
<script>
export default {
data() {
return {
windowHeight: uni.getSystemInfoSync().windowHeight
}
},
methods: {
handleToPwd() {
this.$tab.navigateTo('/pages/mine/pwd/index')
},
handleToUpgrade() {
this.$modal.showToast('模块建设中~')
},
handleCleanTmp() {
this.$modal.showToast('模块建设中~')
},
handleLogout() {
this.$modal.confirm('确定注销并退出系统吗?').then(() => {
this.$store.dispatch('LogOut').then(() => {}).finally(()=>{
this.$tab.reLaunch('/pages/index')
})
})
}
}
}
</script>
<style lang="scss" scoped>
.page {
background-color: #f8f8f8;
}
.item-box {
background-color: #FFFFFF;
margin: 30rpx;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
padding: 10rpx;
border-radius: 8rpx;
color: #303133;
font-size: 32rpx;
}
</style>

704
pages/product/product.vue Normal file
View File

@ -0,0 +1,704 @@
<template>
<view class="container">
<!-- 顶部状态栏/时间 -->
<view class="status-bar">
<text class="time">9:41</text>
</view>
<!-- 搜索栏 -->
<view class="search-bar">
<view class="search-input">
<uni-icons type="search" size="18" color="#999"></uni-icons>
<input
type="text"
placeholder="搜索商品名称、条码"
placeholder-class="placeholder"
v-model="searchText"
@confirm="onSearch"
/>
<uni-icons type="scan" size="18" color="#999"></uni-icons>
</view>
</view>
<!-- 筛选标签 -->
<view class="filter-tabs">
<scroll-view class="tabs-scroll" scroll-x="true">
<view
class="tab-item"
:class="{ active: currentTab === 'all' }"
@tap="switchTab('all')"
>
全部
</view>
<view
class="tab-item"
:class="{ active: currentTab === 'expiring' }"
@tap="switchTab('expiring')"
>
临期/过期<text class="badge">0</text>
</view>
<view
class="tab-item"
:class="{ active: currentTab === 'outstock' }"
@tap="switchTab('outstock')"
>
缺货<text class="badge">0</text>
</view>
<view class="tab-item filter-btn" @tap="showFilter = !showFilter">
<text>筛选</text>
<uni-icons type="arrowdown" size="14" color="#666"></uni-icons>
</view>
</scroll-view>
</view>
<!-- 微店状态栏 -->
<view class="store-status">
<view class="status-content">
<view class="dui">
<image class="dui-img" src="/static/Frame 52.png"></image>
</view>
<view class="status-left">
<text class="status-text">已开启微店商店库</text>
<text class="status-num">微店商品库全226+商品</text>
</view>
</view>
<view class="status-right" @tap="goToSettings">
<text class="set-text">设置</text>
<uni-icons type="forward" size="14" color="#666"></uni-icons>
</view>
</view>
<!-- 商品列表 -->
<scroll-view class="goods-list" scroll-y="true">
<view class="goods-item" v-for="item in goodsList" :key="item.id">
<view class="goods-more-icon" @tap="toggleDeleteMenu(item)">
<uni-icons type="more-filled" size="18"></uni-icons>
</view>
<view class="goods-delete-menu" v-if="item.showDeleteMenu" @tap="showDeleteConfirm(item)">
<text class="delete-menu-text">删除商品</text>
</view>
<view class="goods-top" @tap="goToStockDetail(item)">
<view class="goods-tou">
<image :src="item.mainImage ? 'http://193.112.94.36:8081' + item.mainImage : '/static/687b6f95b14eff60f4b77147b3726ab2.jpg'"></image>
</view>
<view class="goods-info">
<text class="goods-name">{{ item.productName }}</text>
<text class="goods-barcode">{{ item.productBarCode }}</text>
<text class="goods-price">{{ item.storePrice ? item.storePrice.toFixed(2) : '0.00' }}</text>
</view>
</view>
<view class="goods-divider"></view>
<view class="goods-stock" >
<text class="stock-label">总库存</text>
<text class="stock-num">{{ item.stockQuantity }} </text>
<text class="stock-arrow">></text>
</view>
<!-- <view class="goods-actions">
<view class="action-btn add-btn" @tap="addGoodsStock(item)">
<text>添加商品/库存</text>
</view>
</view> -->
</view>
</scroll-view>
<!-- 底部操作按钮 -->
<view class="bottom-actions">
<view class="action-btn more-btn" @tap="showMoreActions(item)">
<text>更多</text>
</view>
<button class="action-button primary" @tap="addNewGoods">
添加商品/库存
</button>
<!-- <button class="action-button" @tap="batchOperation">
批量操作
</button> -->
</view>
<!-- 更多操作弹窗 -->
<uni-popup ref="popup" type="bottom">
<view class="popup-content">
<view class="popup-item" @tap="editGoods(selectedGoods)">
<uni-icons type="compose" size="20" color="#333"></uni-icons>
<text>编辑商品</text>
</view>
<view class="popup-item" @tap="adjustStock(selectedGoods)">
<uni-icons type="plus" size="20" color="#333"></uni-icons>
<text>调整库存</text>
</view>
<view class="popup-item" @tap="deleteGoods(selectedGoods)">
<uni-icons type="trash" size="20" color="#ff4444"></uni-icons>
<text style="color: #ff4444;">删除商品</text>
</view>
<view class="popup-item cancel" @tap="$refs.popup.close()">
<text>取消</text>
</view>
</view>
</uni-popup>
</view>
</template>
<script>
import { getProductList as fetchProductList, deleteProduct } from '@/api/product'
import { getToken, getStoreId } from '@/utils/auth'
export default {
data() {
return {
currentTab: 'all',
searchText: '',
showFilter: false,
selectedGoods: null,
storeId: null, // ID
goodsList: [
{
id: null,
productName: '',
productBarCode: '',
storePrice: 0.00,
stockQuantity: 0,
}
]
}
},
onLoad() {
// ID
const storeId = getStoreId();
if (storeId) {
this.storeId = storeId;
console.log('当前门店ID:', storeId);
} else {
uni.showToast({
title: '请先选择门店',
icon: 'none'
});
setTimeout(() => {
uni.navigateBack();
}, 1500);
return;
}
this.getProductList();
},
onShow() {
this.getProductList();
},
methods: {
async getProductList(searchParams = {}) {
try {
const token = getToken();
console.log('当前Token:', token);
console.log('Token是否存在:', !!token);
console.log('查询参数:', searchParams);
// ID
if (this.storeId) {
searchParams.storeId = this.storeId;
}
const res = await fetchProductList(searchParams);
console.log('商品列表接口返回:', res);
if (res && res.code === 200 && res.data) {
this.goodsList = res.data.map(item => ({
...item,
showDeleteMenu: false
}));
console.log('处理后的商品列表:', this.goodsList);
} else {
uni.showToast({
title: res?.msg || '获取商品列表失败',
icon: 'none'
});
}
} catch (error) {
console.error('获取商品列表失败:', error);
console.error('错误详情:', JSON.stringify(error));
uni.showToast({
title: '网络请求失败',
icon: 'none'
});
}
},
switchTab(tab) {
this.currentTab = tab;
//
console.log('切换到标签:', tab);
},
onSearch() {
console.log('搜索关键词:', this.searchText);
if (!this.searchText || this.searchText.trim() === '') {
this.getProductList();
return;
}
const searchText = this.searchText.trim();
let searchParams = {};
if (/^\d+$/.test(searchText)) {
searchParams = { productName: searchText };
console.log('商品名称模糊查询:', searchParams);
} else {
searchParams = { productBarCode: searchText };
console.log('按条形码精准查询按:', searchParams);
}
this.getProductList(searchParams);
},
goToSettings() {
uni.navigateTo({
url: '/pages/settings/index'
});
},
showMoreActions(goods) {
this.selectedGoods = goods;
this.$refs.popup.open();
},
addGoodsStock(goods) {
console.log('添加商品库存:', goods);
uni.showModal({
title: '添加库存',
content: `${goods.name} 添加库存`,
success: (res) => {
if (res.confirm) {
//
}
}
});
},
goToStockDetail(item) {
// ID
uni.navigateTo({
url: `/pages/edit/edit?id=${item.id}`
});
},
toggleDeleteMenu(item) {
item.showDeleteMenu = !item.showDeleteMenu;
},
showDeleteConfirm(item) {
uni.showModal({
title: '确认删除',
content: `确定要删除商品"${item.productName}"吗?`,
success: async (res) => {
if (res.confirm) {
try {
const result = await deleteProduct(item.id);
console.log('删除商品结果:', result);
if (result && result.code === 200) {
uni.showToast({
title: '删除成功',
icon: 'success'
});
//
this.getProductList();
} else {
uni.showToast({
title: result?.msg || '删除失败',
icon: 'none'
});
}
} catch (error) {
console.error('删除商品失败:', error);
uni.showToast({
title: '删除失败',
icon: 'none'
});
}
}
}
});
},
addNewGoods() {
uni.navigateTo({
url: '/pages/enter/enter'
// url: '/pages/addProduct/addProduct'
});
},
batchOperation() {
uni.showActionSheet({
itemList: ['批量修改价格', '批量修改库存', '批量下架'],
success: (res) => {
console.log('选择了操作:', res.tapIndex);
}
});
},
editGoods(goods) {
this.$refs.popup.close();
uni.navigateTo({
url: `/pages/goods/edit?id=${goods.id}`
});
},
adjustStock(goods) {
this.$refs.popup.close();
uni.showModal({
title: '调整库存',
content: `调整 ${goods.name} 的库存数量`,
editable: true,
placeholderText: '请输入调整数量',
success: (res) => {
if (res.confirm && res.content) {
console.log('调整库存数量:', res.content);
}
}
});
},
deleteGoods(goods) {
this.$refs.popup.close();
uni.showModal({
title: '确认删除',
content: `确定要删除商品"${goods.name}"吗?`,
success: (res) => {
if (res.confirm) {
console.log('删除商品:', goods);
uni.showToast({
title: '删除成功',
icon: 'success'
});
}
}
});
}
}
}
</script>
<style scoped>
.container {
background-color: #f5f5f5;
min-height: 100vh;
}
.goods-tou{
width: 100rpx;
height: 100rpx;
margin-right: 16rpx;
flex-shrink: 0;
}
.goods-tou image {
width: 100%;
height: 100%;
border-radius: 8rpx;
}
/* 状态栏 */
.status-bar {
padding: 10px 16px;
background-color: #fff;
}
.time {
font-size: 16px;
font-weight: 600;
}
/* 搜索栏 */
.search-bar {
padding: 12px 16px;
background-color: #fff;
}
.search-input {
display: flex;
align-items: center;
background-color: #f5f5f5;
border-radius: 8px;
padding: 10px 12px;
}
.search-input input {
flex: 1;
margin-left: 8px;
font-size: 14px;
}
.placeholder {
color: #999;
}
/* 筛选标签 */
.filter-tabs {
background-color: #fff;
padding: 12px 16px;
border-bottom: 1px solid #eee;
}
.tabs-scroll {
white-space: nowrap;
}
.tab-item {
display: inline-block;
padding: 6px 12px;
margin-right: 10px;
border-radius: 16px;
font-size: 14px;
background-color: #f5f5f5;
color: #666;
}
.tab-item.active {
background-color: #e8f4ff;
color: #007aff;
}
.badge {
margin-left: 4px;
color: #ff4444;
}
.filter-btn {
display: inline-flex;
align-items: center;
}
/* 微店状态栏 */
.store-status {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
background-color: #fff;
border-radius: 20rpx;
margin-top: 8px;
border-bottom: 1px solid #eee;
}
.status-content {
display: flex;
align-items: center;
}
.dui {
margin-right: 12rpx;
}
.dui-img {
width: 24rpx;
height: 24rpx;
}
.status-left {
display: flex;
flex-direction: column;
margin-left: 0;
}
.status-text {
font-size: 14px;
color: #333;
}
.status-num {
font-size: 12px;
color: #666;
margin-top: 4px;
}
.status-right {
display: flex;
align-items: center;
margin-right: 1rpx;
}
.set-text {
font-size: 14px;
color: #666;
margin-right: 4px;
}
/* 商品列表 */
.goods-list {
height: calc(100vh - 320px);
padding: 16px;
}
.goods-item {
background-color: #fff;
border-radius: 8px;
padding: 16px;
margin-bottom: 12px;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
position: relative;
}
.goods-more-icon {
position: absolute;
top: 12px;
right: 12px;
z-index: 10;
}
.goods-delete-menu {
position: absolute;
top: 40px;
right: 12px;
background-color: #fff;
border-radius: 8rpx;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.15);
padding: 12rpx 24rpx;
z-index: 9;
}
.delete-menu-text {
font-size: 28rpx;
color: #ff4444;
}
.goods-top {
display: flex;
align-items: flex-start;
margin-bottom: 12px;
}
.goods-info {
flex: 1;
margin-bottom: 0;
}
.goods-name {
display: block;
font-size: 16px;
font-weight: 500;
color: #333;
margin-bottom: 6px;
}
.goods-barcode {
font-size: 12px;
color: #999;
display: block;
margin-bottom: 8px;
}
.goods-price {
font-size: 18px;
color: #ff4444;
font-weight: 600;
}
.goods-divider {
height: 1px;
border-bottom: 1px dashed #e0e0e0;
margin-bottom: 12px;
}
.goods-stock {
display: flex;
align-items: center;
padding: 8px 0;
margin-bottom: 4px;
}
.stock-arrow {
font-size: 20px;
color: #999;
margin-left: auto;
}
.stock-label {
font-size: 14px;
color: #666;
}
.stock-num {
font-size: 16px;
color: #333;
font-weight: 500;
}
.goods-actions {
display: flex;
justify-content: space-between;
}
.action-btn {
padding: 8px 16px;
border-radius: 6px;
font-size: 14px;
display: flex;
align-items: center;
justify-content: center;
}
.more-btn {
background-color: #f5f5f5;
color: #666;
}
.add-btn {
background-color: #e8f4ff;
color: #007aff;
}
/* 底部操作按钮 */
.bottom-actions {
position: fixed;
bottom: 0;
left: 0;
right: 0;
display: flex;
padding: 16px;
background-color: #fff;
border-top: 1px solid #eee;
}
.action-button {
flex: 1;
margin: 0 8px;
padding: 12px;
border-radius: 8px;
font-size: 16px;
background-color: #f5f5f5;
color: #333;
border: none;
}
.action-button.primary {
background-color: #007aff;
color: #fff;
}
/* 弹窗样式 */
.popup-content {
background-color: #fff;
border-radius: 16px 16px 0 0;
padding: 20px;
}
.popup-item {
display: flex;
align-items: center;
padding: 16px 0;
border-bottom: 1px solid #f5f5f5;
font-size: 16px;
}
.popup-item:last-child {
border-bottom: none;
}
.popup-item text {
margin-left: 12px;
}
.popup-item.cancel {
justify-content: center;
color: #666;
}
</style>

189
pages/register.vue Normal file
View File

@ -0,0 +1,189 @@
<template>
<view class="normal-login-container">
<view class="logo-content align-center justify-center flex">
<image style="width: 100rpx;height: 100rpx;" :src="globalConfig.appInfo.logo" mode="widthFix">
</image>
<text class="title">若依移动端注册</text>
</view>
<view class="login-form-content">
<view class="input-item flex align-center">
<view class="iconfont icon-user icon"></view>
<input v-model="registerForm.username" class="input" type="text" placeholder="请输入账号" maxlength="30" />
</view>
<view class="input-item flex align-center">
<view class="iconfont icon-password icon"></view>
<input v-model="registerForm.password" type="password" class="input" placeholder="请输入密码" maxlength="20" />
</view>
<view class="input-item flex align-center">
<view class="iconfont icon-password icon"></view>
<input v-model="registerForm.confirmPassword" type="password" class="input" placeholder="请输入重复密码" maxlength="20" />
</view>
<view class="input-item flex align-center" style="width: 60%;margin: 0px;" v-if="captchaEnabled">
<view class="iconfont icon-code icon"></view>
<input v-model="registerForm.code" type="number" class="input" placeholder="请输入验证码" maxlength="4" />
<view class="login-code">
<image :src="codeUrl" @click="getCode" class="login-code-img"></image>
</view>
</view>
<view class="action-btn">
<button @click="handleRegister()" class="register-btn cu-btn block bg-blue lg round">注册</button>
</view>
</view>
<view class="xieyi text-center">
<text @click="handleUserLogin" class="text-blue">使用已有账号登录</text>
</view>
</view>
</template>
<script>
import { getCodeImg, register } from '@/api/login'
export default {
data() {
return {
codeUrl: "",
captchaEnabled: true,
globalConfig: getApp().globalData.config,
registerForm: {
username: "",
password: "",
confirmPassword: "",
code: "",
uuid: ""
}
}
},
created() {
this.getCode()
},
methods: {
//
handleUserLogin() {
this.$tab.navigateTo(`/pages/login`)
},
//
getCode() {
getCodeImg().then(res => {
this.captchaEnabled = res.captchaEnabled === undefined ? true : res.captchaEnabled
if (this.captchaEnabled) {
this.codeUrl = 'data:image/gif;base64,' + res.img
this.registerForm.uuid = res.uuid
}
})
},
//
async handleRegister() {
if (this.registerForm.username === "") {
this.$modal.msgError("请输入您的账号")
} else if (this.registerForm.password === "") {
this.$modal.msgError("请输入您的密码")
} else if (this.registerForm.confirmPassword === "") {
this.$modal.msgError("请再次输入您的密码")
} else if (this.registerForm.password !== this.registerForm.confirmPassword) {
this.$modal.msgError("两次输入的密码不一致")
} else if (this.registerForm.code === "" && this.captchaEnabled) {
this.$modal.msgError("请输入验证码")
} else {
this.$modal.loading("注册中,请耐心等待...")
this.register()
}
},
//
async register() {
register(this.registerForm).then(res => {
this.$modal.closeLoading()
uni.showModal({
title: "系统提示",
content: "恭喜你,您的账号 " + this.registerForm.username + " 注册成功!",
success: function (res) {
if (res.confirm) {
uni.redirectTo({ url: `/pages/login` });
}
}
})
}).catch(() => {
if (this.captchaEnabled) {
this.getCode()
}
})
}
}
}
</script>
<style lang="scss" scoped>
page {
background-color: #ffffff;
}
.normal-login-container {
width: 100%;
.logo-content {
width: 100%;
font-size: 21px;
text-align: center;
padding-top: 15%;
image {
border-radius: 4px;
}
.title {
margin-left: 10px;
}
}
.login-form-content {
text-align: center;
margin: 20px auto;
margin-top: 15%;
width: 80%;
.input-item {
margin: 20px auto;
background-color: #f5f6f7;
height: 45px;
border-radius: 20px;
.icon {
font-size: 38rpx;
margin-left: 10px;
color: #999;
}
.input {
width: 100%;
font-size: 14px;
line-height: 20px;
text-align: left;
padding-left: 15px;
}
}
.register-btn {
margin-top: 40px;
height: 45px;
}
.xieyi {
color: #333;
margin-top: 20px;
}
.login-code {
height: 38px;
float: right;
.login-code-img {
height: 38px;
position: absolute;
margin-left: 10px;
width: 200rpx;
}
}
}
}
</style>

183
pages/settings/settings.vue Normal file
View File

@ -0,0 +1,183 @@
<template>
<view class="settings-container">
<!-- 页面标题 -->
<view class="page-header">
<text class="page-title">设置</text>
</view>
<!-- 通用模块 -->
<view class="settings-section">
<view class="section-header">
<text class="section-title">通用</text>
</view>
<view class="settings-list">
<view class="list-item" @click="navigateTo('个人信息')">
<text class="item-text">个人信息</text>
<text class="iconfont icon-arrow"></text>
</view>
<view class="list-item" @click="navigateTo('店铺信息')">
<text class="item-text">店铺信息</text>
<text class="iconfont icon-arrow"></text>
</view>
<view class="list-item" @click="navigateTo('子账号管理')">
<text class="item-text">子账号管理</text>
<text class="iconfont icon-arrow"></text>
</view>
</view>
</view>
<!-- 门店模块 -->
<view class="settings-section">
<view class="section-header">
<text class="section-title">门店</text>
</view>
<view class="settings-list">
<view class="list-item" @click="navigateTo('开关门设置')">
<text class="item-text">开关门设置</text>
<text class="iconfont icon-arrow"></text>
</view>
<view class="list-item" @click="navigateTo('进店语音设置')">
<text class="item-text">进店语音设置</text>
<text class="iconfont icon-arrow"></text>
</view>
<view class="list-item" @click="navigateTo('推荐设置')">
<text class="item-text">推荐设置</text>
<text class="iconfont icon-arrow"></text>
</view>
</view>
</view>
<!-- 交易模块 -->
<view class="settings-section">
<view class="section-header">
<text class="section-title">交易</text>
</view>
<view class="settings-list">
<view class="list-item" @click="navigateTo('支付方式认证')">
<text class="item-text">支付方式认证</text>
<text class="iconfont icon-arrow"></text>
</view>
<view class="list-item" @click="navigateTo('自动化扣')">
<text class="item-text">自动化扣</text>
<text class="iconfont icon-arrow"></text>
</view>
<view class="list-item" @click="navigateTo('音频级别设置')">
<text class="item-text">音频级别设置</text>
<text class="iconfont icon-arrow"></text>
</view>
<view class="list-item" @click="navigateTo('非值守期间现金收银')">
<text class="item-text">非值守期间现金收银</text>
<text class="iconfont icon-arrow"></text>
</view>
</view>
</view>
</view>
</template>
<script>
export default {
data() {
return {
//
}
},
methods: {
navigateTo(pageName) {
//
uni.showToast({
title: `跳转到${pageName}`,
icon: 'none',
duration: 1000
});
//
// switch(pageName) {
// case '':
// uni.navigateTo({ url: '/pages/user/profile' });
// break;
// case '':
// uni.navigateTo({ url: '/pages/shop/info' });
// break;
// // ...
// }
}
}
}
</script>
<style lang="scss" scoped>
.settings-container {
background-color: #f5f5f5;
min-height: 100vh;
padding-bottom: 20rpx;
.page-header {
padding: 30rpx 40rpx;
background-color: #ffffff;
border-bottom: 1rpx solid #e0e0e0;
.page-title {
font-size: 40rpx;
font-weight: 600;
color: #333333;
}
}
.settings-section {
margin-top: 30rpx;
background-color: #ffffff;
border-radius: 16rpx;
overflow: hidden;
margin-left: 30rpx;
margin-right: 30rpx;
.section-header {
padding: 28rpx 32rpx;
border-bottom: 1rpx solid #f0f0f0;
.section-title {
font-size: 32rpx;
font-weight: 600;
color: #333333;
}
}
.settings-list {
.list-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 30rpx 32rpx;
border-bottom: 1rpx solid #f0f0f0;
&:last-child {
border-bottom: none;
}
&:active {
background-color: #f8f8f8;
}
.item-text {
font-size: 30rpx;
color: #333333;
}
.iconfont {
font-size: 36rpx;
color: #999999;
}
}
}
}
}
/* 响应式适配 */
@media (min-width: 768px) {
.settings-container {
max-width: 750px;
margin: 0 auto;
box-shadow: 0 0 20rpx rgba(0, 0, 0, 0.05);
}
}
</style>

View File

@ -0,0 +1,221 @@
<template>
<view class="store-select-container">
<!-- 自定义导航栏 -->
<view class="navbar">
<view class="nav-left" @click="goBack">
<uni-icons type="left" size="20" color="#fff"></uni-icons>
</view>
<view class="nav-title">选择门店</view>
<view class="nav-right"></view>
</view>
<view class="store-list">
<view
class="store-item"
v-for="(item, index) in storeList"
:key="index"
@click="selectStore(item)"
>
<view class="store-info">
<text class="store-name">{{ item.storeName }}</text>
<text class="store-code">编码{{ item.storeCode }}</text>
</view>
<uni-icons type="right" size="20" color="#999"></uni-icons>
</view>
</view>
<view class="empty-state" v-if="storeList.length === 0">
<text class="empty-text">暂无门店数据</text>
</view>
</view>
</template>
<script>
import { getStoreList } from '@/api/store'
import { getUserId, setStoreId, setStoreInfo, getStoreId } from '@/utils/auth'
export default {
data() {
return {
userId: null,
storeList: []
}
},
onLoad() {
// ID
const userId = getUserId();
if (userId) {
this.userId = userId;
this.loadStoreList();
} else {
uni.showToast({
title: '用户ID不存在',
icon: 'none'
});
setTimeout(() => {
uni.navigateBack();
}, 1500);
}
},
methods: {
loadStoreList() {
uni.showLoading({
title: '加载中...'
});
// ID
getStoreList(String(this.userId)).then(res => {
uni.hideLoading();
if (res && res.code === 200) {
//
if (Array.isArray(res.data)) {
this.storeList = res.data;
} else if (res.data && res.data.stores) {
this.storeList = res.data.stores;
}
console.log('门店列表:', this.storeList);
} else {
uni.showToast({
title: res?.msg || '获取门店列表失败',
icon: 'none'
});
}
}).catch(error => {
uni.hideLoading();
console.error('获取门店列表失败:', error);
uni.showToast({
title: '网络请求失败',
icon: 'none'
});
});
},
goBack() {
//
const storeId = getStoreId();
if (storeId) {
//
uni.showToast({
title: '请选择门店',
icon: 'none'
});
} else {
//
uni.navigateBack();
}
},
selectStore(store) {
console.log('选择门店:', store);
// ID
setStoreId(store.storeId);
setStoreInfo(store);
//
uni.showToast({
title: '已选择门店',
icon: 'success'
});
//
setTimeout(() => {
uni.switchTab({
url: '/pages/index'
});
}, 1000);
}
}
}
</script>
<style scoped>
.store-select-container {
min-height: 100vh;
background-color: #f5f5f5;
padding-top: 44px;
}
.navbar {
width: 100%;
height: 44px;
background-color: #e60012;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 15px;
box-sizing: border-box;
position: fixed;
top: 0;
left: 0;
z-index: 999;
}
.nav-left, .nav-right {
display: flex;
align-items: center;
color: #fff;
}
.nav-title {
font-size: 18px;
font-weight: bold;
color: #fff;
}
.store-list {
background-color: #fff;
border-radius: 12px;
margin: 20px;
overflow: hidden;
}
.store-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20px;
border-bottom: 1px solid #f0f0f0;
transition: background-color 0.3s;
}
.store-item:active {
background-color: #f9f9f9;
}
.store-item:last-child {
border-bottom: none;
}
.store-info {
flex: 1;
display: flex;
flex-direction: column;
}
.store-name {
font-size: 16px;
color: #333;
font-weight: 500;
margin-bottom: 8px;
}
.store-code {
font-size: 14px;
color: #999;
}
.empty-state {
text-align: center;
padding: 60px 20px;
background-color: #fff;
border-radius: 12px;
margin: 20px;
}
.empty-text {
font-size: 14px;
color: #999;
}
</style>

319
pages/user/user.vue Normal file
View File

@ -0,0 +1,319 @@
<template>
<view class="container">
<!-- 个人信息标题模块 -->
<view class="header-module">
<text class="header-title">个人信息</text>
</view>
<view class="one-module">
<view class="touXiang">
<view class="gearOne"><uni-icons type="camera-filled" size="20"></uni-icons></view>
</view>
</view>
<!-- 账户信息模块 -->
<view class="info-module">
<view class="module-item">
<text class="item-label">昵称</text>
<text class="item-value">名称</text>
</view>
<view class="module-item">
<text class="item-label">绑定微信号</text>
<text class="item-value">名称</text>
</view>
</view>
<!-- 账户安全模块 -->
<view class="security-module">
<view class="module-title">账户安全</view>
<view class="module-item clickable" @tap="handleChangePhone">
<text class="item-label">修改手机号</text>
<view class="item-right">
<text class="arrow">></text>
</view>
</view>
<view class="module-item clickable" @tap="handleChangePassword">
<text class="item-label">修改密码</text>
<view class="item-right">
<text class="arrow">></text>
</view>
</view>
</view>
<!-- 设备管理模块 -->
<view class="device-module">
<view class="module-title">设备管理</view>
<view class="module-item clickable" @tap="handleDeviceManage">
<text class="item-label">设备管理</text>
<view class="item-right">
<text class="arrow">></text>
</view>
</view>
</view>
<!-- 账号管理模块 -->
<view class="account-module">
<view class="module-title">账号管理</view>
<view class="module-item clickable last-item" @tap="handleCancelAccount">
<text class="item-label">注销账号</text>
<view class="item-right">
<text class="arrow">></text>
</view>
</view>
</view>
<!-- 退出登录模块 -->
<view class="logout-module">
<view class="logout-btn" @tap="handleLogout">
<text class="logout-text">退出登录</text>
</view>
</view>
<!-- 底部提示模块 -->
<view class="footer-module">
<text class="footer-text">这不是我的账号</text>
</view>
</view>
</template>
<script>
export default {
data() {
return {}
},
methods: {
//
handleChangePhone() {
uni.showToast({
title: '跳转到修改手机号页面',
icon: 'none'
})
},
//
handleChangePassword() {
uni.showToast({
title: '跳转到修改密码页面',
icon: 'none'
})
},
//
handleDeviceManage() {
uni.showToast({
title: '跳转到设备管理页面',
icon: 'none'
})
},
//
handleCancelAccount() {
uni.showModal({
title: '提示',
content: '确定要注销账号吗?此操作不可恢复。',
success: (res) => {
if (res.confirm) {
uni.showToast({
title: '账号注销申请已提交',
icon: 'none'
})
}
}
})
},
// 退
handleLogout() {
uni.showModal({
title: '提示',
content: '确定要退出登录吗?',
success: (res) => {
if (res.confirm) {
uni.showToast({
title: '退出登录成功',
icon: 'success',
duration: 1500,
success: () => {
setTimeout(() => {
uni.reLaunch({ url: '/pages/login/login' })
}, 1500)
}
})
}
}
})
}
}
}
</script>
<style>
.container {
background-color: #f5f5f5;
min-height: 100vh;
}
.touXiang{
width: 150rpx;
height: 150rpx;
border-radius: 50%;
background-color: #ffffff;
background-image: url('/static/687b6f95b14eff60f4b77147b3726ab2.jpg');
background-size: cover;
background-position: center;
background-repeat: no-repeat;
display: flex;
align-items: center;
justify-content: center;
margin: 40rpx auto;
position: relative;
}
.gearOne{
position: absolute;
right: 0;
top: 0;
width: 40rpx;
height: 40rpx;
border-radius: 50%;
background-color: #ffffff;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
}
/* ========== 状态栏模块 ========== */
.status-bar {
background-color: #ffffff;
padding: 40rpx 0 20rpx;
display: flex;
justify-content: center;
align-items: center;
}
.time {
font-size: 34rpx;
font-weight: bold;
color: #000000;
}
/* ========== 标题模块 ========== */
.header-module {
background-color: cornflowerblue;
padding: 40rpx 40rpx 60rpx;
border-bottom: 2rpx solid #f5f5f5;
text-align: center;
}
.header-title {
font-size: 30rpx;
/* font-weight: bold; */
color: #000000;
}
/* ========== 通用模块样式 ========== */
.info-module,
.security-module,
.device-module,
.account-module {
background-color: #ffffff;
margin-top: 20rpx;
padding: 0 40rpx;
margin: 20rpx;
border-radius: 16rpx;
}
/* 模块标题 */
.module-title {
font-size: 30rpx;
color: #999999;
padding: 30rpx 0 20rpx;
}
/* 模块项 */
.module-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 36rpx 0;
border-bottom: 2rpx solid #f0f0f0;
}
/* 最后一项去掉底部边框 */
.last-item {
border-bottom: none;
}
/* 点击效果 */
.clickable:active {
background-color: rgba(0, 0, 0, 0.02);
}
/* 标签样式 */
.item-label {
font-size: 34rpx;
color: #333333;
}
/* 值样式 */
.item-value {
font-size: 34rpx;
color: #666666;
}
/* 右侧内容 */
.item-right {
display: flex;
align-items: center;
}
/* 箭头样式 */
.arrow {
font-size: 36rpx;
color: #cccccc;
margin-left: 20rpx;
}
/* ========== 账户信息模块 ========== */
.info-module {
margin-top: 0;
border-top: 2rpx solid #f5f5f5;
}
/* ========== 退出登录模块 ========== */
.logout-module {
margin-top: 40rpx;
padding: 0 40rpx;
}
.logout-btn {
width: 100%;
height: 88rpx;
background-color: #ffffff;
border-radius: 12rpx;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
}
.logout-btn:active {
background-color: rgba(0, 0, 0, 0.02);
}
.logout-text {
font-size: 34rpx;
color: red;
}
/* ========== 底部提示模块 ========== */
.footer-module {
margin-top: 40rpx;
padding: 0 40rpx 60rpx;
display: flex;
justify-content: center;
}
.footer-text {
font-size: 20rpx;
color: limegreen;
}
</style>

View File

@ -0,0 +1,225 @@
<template>
<view class="user-stores-container">
<!-- 页面标题 -->
<view class="page-header">
<text class="page-title">用户门店关联</text>
<view class="header-right">
<button class="save-btn" @click="saveUserStores"></button>
</view>
</view>
<!-- 门店列表 -->
<view class="stores-list">
<view class="store-item" v-for="store in stores" :key="store.storeId">
<view class="store-info">
<text class="store-name">{{ store.storeName }}</text>
<text class="store-code">编码{{ store.storeCode }}</text>
</view>
<uni-data-checkbox
:value="store.storeId"
:checked="isStoreChecked(store.storeId)"
@change="onCheckboxChange"
:name="'store-' + store.storeId"
></uni-data-checkbox>
</view>
</view>
<!-- 空状态 -->
<view class="empty-state" v-if="stores.length === 0">
<text class="empty-text">暂无门店数据</text>
</view>
</view>
</template>
<script>
export default {
data() {
return {
form: {}, //
stores: [], //
userId: null,
selectedStoreIds: [] // ID
}
},
onLoad(options) {
// ID
this.userId = options.userId || 2; // 使2
this.loadUserStores();
},
methods: {
//
loadUserStores() {
uni.showLoading({
title: '加载中...'
});
// API
// API
// {
// "code": 200,
// "data": {
// "allStores": [{storeId: 1, storeName: "621", storeCode: "6code"}, ...],
// "stores": [{storeId: 1, storeName: "621", storeCode: "6code"}, ...],
// "userId": 2
// },
// "msg": ""
// }
// 使setTimeout
setTimeout(() => {
//
const mockResponse = {
code: 200,
data: {
allStores: [
{storeId: 1, storeName: "门店621", storeCode: "门店6code"},
{storeId: 2, storeName: "门店622", storeCode: "门店6code2"},
{storeId: 3, storeName: "门店623", storeCode: "门店6code3"}
],
stores: [
{storeId: 1, storeName: "门店621", storeCode: "门店6code"}
],
userId: 2
},
msg: "操作成功"
};
//
this.form = mockResponse.data.stores || {};
this.stores = mockResponse.data.allStores || [];
// ID
this.selectedStoreIds = mockResponse.data.stores.map(store => store.storeId);
uni.hideLoading();
}, 1000);
},
//
isStoreChecked(storeId) {
return this.selectedStoreIds.includes(storeId);
},
//
onCheckboxChange(e) {
const storeId = e.value;
const isChecked = e.checked;
if (isChecked) {
//
if (!this.selectedStoreIds.includes(storeId)) {
this.selectedStoreIds.push(storeId);
}
} else {
//
this.selectedStoreIds = this.selectedStoreIds.filter(id => id !== storeId);
}
},
//
saveUserStores() {
uni.showLoading({
title: '保存中...'
});
// API
setTimeout(() => {
// selectedStoreIds
console.log('保存的门店ID:', this.selectedStoreIds);
uni.hideLoading();
uni.showToast({
title: '保存成功',
icon: 'success'
});
//
setTimeout(() => {
uni.navigateBack();
}, 1500);
}, 1000);
}
}
}
</script>
<style lang="scss" scoped>
.user-stores-container {
background-color: #f5f5f5;
min-height: 100vh;
padding-bottom: 20rpx;
}
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 30rpx 40rpx;
background-color: #ffffff;
border-bottom: 1rpx solid #e0e0e0;
}
.page-title {
font-size: 40rpx;
font-weight: 600;
color: #333333;
}
.save-btn {
padding: 12rpx 30rpx;
background-color: #007aff;
color: #ffffff;
border: none;
border-radius: 8rpx;
font-size: 28rpx;
}
.stores-list {
margin: 20rpx;
background-color: #ffffff;
border-radius: 16rpx;
overflow: hidden;
}
.store-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 30rpx 40rpx;
border-bottom: 1rpx solid #f0f0f0;
&:last-child {
border-bottom: none;
}
}
.store-info {
flex: 1;
}
.store-name {
display: block;
font-size: 32rpx;
font-weight: 600;
color: #333333;
margin-bottom: 10rpx;
}
.store-code {
display: block;
font-size: 28rpx;
color: #999999;
}
.empty-state {
display: flex;
justify-content: center;
align-items: center;
height: 300rpx;
}
.empty-text {
font-size: 30rpx;
color: #999999;
}
</style>

182
pages/work/index.vue Normal file
View File

@ -0,0 +1,182 @@
<template>
<view class="work-container">
<!-- 轮播图 -->
<uni-swiper-dot class="uni-swiper-dot-box" :info="data" :current="current" field="content">
<swiper class="swiper-box" :current="swiperDotIndex" @change="changeSwiper">
<swiper-item v-for="(item, index) in data" :key="index">
<view class="swiper-item" @click="clickBannerItem(item)">
<image :src="item.image" mode="aspectFill" :draggable="false" />
</view>
</swiper-item>
</swiper>
</uni-swiper-dot>
<!-- 宫格组件 -->
<uni-section title="系统管理" type="line"></uni-section>
<view class="grid-body">
<uni-grid :column="4" :showBorder="false" @change="changeGrid">
<uni-grid-item>
<view class="grid-item-box">
<uni-icons type="person-filled" size="30"></uni-icons>
<text class="text">用户管理</text>
</view>
</uni-grid-item>
<uni-grid-item>
<view class="grid-item-box">
<uni-icons type="staff-filled" size="30"></uni-icons>
<text class="text">角色管理</text>
</view>
</uni-grid-item>
<uni-grid-item>
<view class="grid-item-box">
<uni-icons type="color" size="30"></uni-icons>
<text class="text">菜单管理</text>
</view>
</uni-grid-item>
<uni-grid-item>
<view class="grid-item-box">
<uni-icons type="settings-filled" size="30"></uni-icons>
<text class="text">部门管理</text>
</view>
</uni-grid-item>
<uni-grid-item>
<view class="grid-item-box">
<uni-icons type="heart-filled" size="30"></uni-icons>
<text class="text">岗位管理</text>
</view>
</uni-grid-item>
<uni-grid-item>
<view class="grid-item-box">
<uni-icons type="bars" size="30"></uni-icons>
<text class="text">字典管理</text>
</view>
</uni-grid-item>
<uni-grid-item>
<view class="grid-item-box">
<uni-icons type="gear-filled" size="30"></uni-icons>
<text class="text">参数设置</text>
</view>
</uni-grid-item>
<uni-grid-item>
<view class="grid-item-box">
<uni-icons type="chat-filled" size="30"></uni-icons>
<text class="text">通知公告</text>
</view>
</uni-grid-item>
<uni-grid-item>
<view class="grid-item-box">
<uni-icons type="wallet-filled" size="30"></uni-icons>
<text class="text">日志管理</text>
</view>
</uni-grid-item>
</uni-grid>
</view>
</view>
</template>
<script>
export default {
data() {
return {
current: 0,
swiperDotIndex: 0,
data: [{
image: '/static/images/banner/banner01.jpg'
},
{
image: '/static/images/banner/banner02.jpg'
},
{
image: '/static/images/banner/banner03.jpg'
}
]
}
},
methods: {
clickBannerItem(item) {
console.info(item)
},
changeSwiper(e) {
this.current = e.detail.current
},
changeGrid(e) {
this.$modal.showToast('模块建设中~')
}
}
}
</script>
<style lang="scss" scoped>
/* #ifndef APP-NVUE */
page {
display: flex;
flex-direction: column;
box-sizing: border-box;
background-color: #fff;
min-height: 100%;
height: auto;
}
view {
font-size: 14px;
line-height: inherit;
}
/* #endif */
.text {
text-align: center;
font-size: 26rpx;
margin-top: 10rpx;
}
.grid-item-box {
flex: 1;
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: column;
align-items: center;
justify-content: center;
padding: 15px 0;
}
.uni-margin-wrap {
width: 690rpx;
width: 100%;
;
}
.swiper {
height: 300rpx;
}
.swiper-box {
height: 150px;
}
.swiper-item {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: column;
justify-content: center;
align-items: center;
color: #fff;
height: 300rpx;
line-height: 300rpx;
}
@media screen and (min-width: 500px) {
.uni-swiper-dot-box {
width: 400px;
/* #ifndef APP-NVUE */
margin: 0 auto;
/* #endif */
margin-top: 8px;
}
.image {
width: 100%;
}
}
</style>

39
permission.js Normal file
View File

@ -0,0 +1,39 @@
import { getToken } from '@/utils/auth'
// 登录页面
const loginPage = "/pages/login"
// 页面白名单
const whiteList = [
'/pages/login', '/pages/register', '/pages/common/webview/index'
]
// 检查地址白名单
function checkWhite(url) {
const path = url.split('?')[0]
return whiteList.indexOf(path) !== -1
}
// 页面跳转验证拦截器
let list = ["navigateTo", "redirectTo", "reLaunch", "switchTab"]
list.forEach(item => {
uni.addInterceptor(item, {
invoke(to) {
if (getToken()) {
if (to.url === loginPage) {
uni.reLaunch({ url: "/" })
}
return true
} else {
if (checkWhite(to.url)) {
return true
}
uni.reLaunch({ url: loginPage })
return false
}
},
fail(err) {
console.log(err)
}
})
})

60
plugins/auth.js Normal file
View File

@ -0,0 +1,60 @@
import store from '@/store'
function authPermission(permission) {
const all_permission = "*:*:*"
const permissions = store.getters && store.getters.permissions
if (permission && permission.length > 0) {
return permissions.some(v => {
return all_permission === v || v === permission
})
} else {
return false
}
}
function authRole(role) {
const super_admin = "admin"
const roles = store.getters && store.getters.roles
if (role && role.length > 0) {
return roles.some(v => {
return super_admin === v || v === role
})
} else {
return false
}
}
export default {
// 验证用户是否具备某权限
hasPermi(permission) {
return authPermission(permission)
},
// 验证用户是否含有指定权限,只需包含其中一个
hasPermiOr(permissions) {
return permissions.some(item => {
return authPermission(item)
})
},
// 验证用户是否含有指定权限,必须全部拥有
hasPermiAnd(permissions) {
return permissions.every(item => {
return authPermission(item)
})
},
// 验证用户是否具备某角色
hasRole(role) {
return authRole(role)
},
// 验证用户是否含有指定角色,只需包含其中一个
hasRoleOr(roles) {
return roles.some(item => {
return authRole(item)
})
},
// 验证用户是否含有指定角色,必须全部拥有
hasRoleAnd(roles) {
return roles.every(item => {
return authRole(item)
})
}
}

14
plugins/index.js Normal file
View File

@ -0,0 +1,14 @@
import tab from './tab'
import auth from './auth'
import modal from './modal'
export default {
install(Vue) {
// 页签操作
Vue.prototype.$tab = tab
// 认证对象
Vue.prototype.$auth = auth
// 模态框对象
Vue.prototype.$modal = modal
}
}

78
plugins/modal.js Normal file
View File

@ -0,0 +1,78 @@
export default {
// 消息提示
msg(content) {
uni.showToast({
title: content,
icon: 'none'
})
},
// 错误消息
msgError(content) {
uni.showToast({
title: content,
icon: 'error'
})
},
// 成功消息
msgSuccess(content) {
uni.showToast({
title: content,
icon: 'success'
})
},
// 隐藏消息
hideMsg(content) {
uni.hideToast()
},
// 弹出提示
alert(content, title) {
uni.showModal({
title: title || '系统提示',
content: content,
showCancel: false
})
},
// 确认窗体
confirm(content, title) {
return new Promise((resolve, reject) => {
uni.showModal({
title: title || '系统提示',
content: content,
cancelText: '取消',
confirmText: '确定',
success: function(res) {
if (res.confirm) {
resolve(res.confirm)
}
}
})
})
},
// 提示信息
showToast(option) {
if (typeof option === "object") {
uni.showToast(option)
} else {
uni.showToast({
title: option,
icon: "none",
duration: 2500
})
}
},
// 打开遮罩层
loading(content) {
uni.showLoading({
title: content,
icon: 'none'
})
},
// 关闭遮罩层
closeLoading() {
try {
uni.hideLoading()
} catch (e) {
console.log(e)
}
}
}

30
plugins/tab.js Normal file
View File

@ -0,0 +1,30 @@
export default {
// 关闭所有页面,打开到应用内的某个页面
reLaunch(url) {
return uni.reLaunch({
url: url
})
},
// 跳转到tabBar页面并关闭其他所有非tabBar页面
switchTab(url) {
return uni.switchTab({
url: url
})
},
// 关闭当前页面,跳转到应用内的某个页面
redirectTo(url) {
return uni.redirectTo({
url: url
})
},
// 保留当前页面,跳转到应用内的某个页面
navigateTo(url) {
return uni.navigateTo({
url: url
})
},
// 关闭当前页面,返回上一页面或多级页面
navigateBack() {
return uni.navigateBack()
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

BIN
static/Frame 52.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 557 B

BIN
static/Frame 6.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
static/Frame 61.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

BIN
static/Frame 63.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

BIN
static/Frame 64.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

BIN
static/Frame 65-1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

BIN
static/Frame 65.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

BIN
static/Frame 66.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

BIN
static/Frame 67.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

BIN
static/Frame 68.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

BIN
static/Frame 69.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

BIN
static/Frame 7.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

BIN
static/Frame 70.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

BIN
static/Frame 71.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

BIN
static/Frame 72.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

BIN
static/Frame 74.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 347 B

BIN
static/Frame 75.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 867 B

BIN
static/Frame 76.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

BIN
static/Frame 77.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

BIN
static/Frame 78.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

BIN
static/Frame 79.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

BIN
static/Frame 8.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

BIN
static/Rectangle 283.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

BIN
static/Rectangle 284.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
static/Rectangle 290.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 KiB

BIN
static/Union (1).png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 263 B

BIN
static/Union.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 263 B

BIN
static/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

90
static/font/iconfont.css Normal file
View File

@ -0,0 +1,90 @@
@font-face {
font-family: "iconfont";
src: url('@/static/font/iconfont.ttf') format('truetype');
}
.iconfont {
font-family: "iconfont" !important;
font-size: 16px;
display: inline-block;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.icon-user:before {
content: "\e7ae";
}
.icon-password:before {
content: "\e8b2";
}
.icon-code:before {
content: "\e699";
}
.icon-setting:before {
content: "\e6cc";
}
.icon-share:before {
content: "\e739";
}
.icon-edit:before {
content: "\e60c";
}
.icon-version:before {
content: "\e63f";
}
.icon-service:before {
content: "\e6ff";
}
.icon-friendfill:before {
content: "\e726";
}
.icon-community:before {
content: "\e741";
}
.icon-people:before {
content: "\e736";
}
.icon-dianzan:before {
content: "\ec7f";
}
.icon-right:before {
content: "\e7eb";
}
.icon-logout:before {
content: "\e61d";
}
.icon-help:before {
content: "\e616";
}
.icon-github:before {
content: "\e628";
}
.icon-aixin:before {
content: "\e601";
}
.icon-clean:before {
content: "\e607";
}
.icon-refresh:before {
content: "\e604";
}

BIN
static/font/iconfont.ttf Normal file

Binary file not shown.

BIN
static/image 71.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 525 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

BIN
static/images/profile.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

75
static/index.html Normal file
View File

@ -0,0 +1,75 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="renderer" content="webkit">
<title><%= htmlWebpackPlugin.options.title %></title>
<link rel="shortcut icon" type="image/x-icon" href="<%= BASE_URL %>static/favicon.ico">
<script>
var coverSupport = 'CSS' in window && typeof CSS.supports === 'function' && (CSS.supports('top: env(a)') || CSS.supports('top: constant(a)'))
document.write('<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0' + (coverSupport ? ', viewport-fit=cover' : '') + '" />')
</script>
<link rel="stylesheet" href="<%= BASE_URL %>static/index.<%= VUE_APP_INDEX_CSS_HASH %>.css" />
<style>
/* 重置样式,确保跨平台一致性 */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
-webkit-tap-highlight-color: transparent;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
html, body {
width: 100%;
height: 100%;
overflow-x: hidden;
-webkit-overflow-scrolling: touch;
}
/* 修复移动端1px边框问题 */
.border-1px {
position: relative;
}
.border-1px::after {
content: '';
position: absolute;
top: 0;
left: 0;
width: 200%;
height: 200%;
border: 1px solid #e0e0e0;
transform: scale(0.5);
transform-origin: 0 0;
pointer-events: none;
}
/* 修复移动端字体模糊问题 */
.text-blur-fix {
-webkit-transform: translateZ(0);
transform: translateZ(0);
}
/* 修复移动端滚动问题 */
.scroll-fix {
-webkit-overflow-scrolling: touch;
overflow-y: auto;
overflow-x: hidden;
}
/* 修复移动端点击延迟 */
.fast-click {
touch-action: manipulation;
}
</style>
</head>
<body>
<noscript>
<strong>本站点必须要开启JavaScript才能运行.</strong>
</noscript>
<div id="app"></div>
</body>
</html>

BIN
static/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

BIN
static/logo200.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

BIN
static/pei.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 866 B

Some files were not shown because too many files have changed in this diff Show More