Compare commits

...

No commits in common. "main" and "master" have entirely different histories.
main ... master

204 changed files with 38800 additions and 91 deletions

31
.gitignore vendored Normal file
View File

@ -0,0 +1,31 @@
######################################################################
# Build Tools
/unpackage/*
/node_modules/*
######################################################################
# Development Tools
/.idea/*
/.vscode/*
/.hbuilderx/*
package-lock.json
yarn.lock
# 微信小程序CI上传密钥敏感文件禁止提交
private.key
# 可选:如果有测试环境的密钥文件,也一起忽略
private-test.key
# 其他小程序开发常见忽略项(建议一并添加,避免冗余文件提交)
node_modules/
dist/
unpackage/
.DS_Store
*.log
.vscode/
.project

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. **保持深色模式不变**
- 深色模式已经很清晰,无需修改
### 预期效果:
- 模块之间区分更明显
- 视觉层次更清晰
- 保持简洁风格的同时提升可读性

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
})
}

355
api/product.js Normal file
View File

@ -0,0 +1,355 @@
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
if (error) {
toast('后端接口连接异常')
reject('后端接口连接异常')
return
}
console.log('=== 8081端口响应详情 ===')
// console.log('响应状态码:', res.statusCode)
console.log('响应头:', res.header)
console.log('响应数据:', res.data)
console.log('响应数据类型:', typeof res.data)
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://193.112.94.36:8081',
url: '/mall/product/importRecord',
method: 'get'
});
}
// 批量删除商品
export function batchDeleteProduct(ids) {
return request8081({
baseUrl: 'http://193.112.94.36:8081',
url: '/mall/product/batchDelete',
method: 'delete',
data: { ids: ids }
})
}
// 添加品牌
export function addBrand(data) {
return request8081({
baseUrl: 'http://193.112.94.36:8081',
url: '/mall/brand/add',
method: 'post',
data: data
})
}
// 获取品牌列表
export function getBrandList(params) {
return request8081({
baseUrl: 'http://193.112.94.36:8081',
url: '/mall/brand/list',
method: 'get',
params: params
})
}
// 获取品牌树状结构
export function getBrandTree(storeId, brandName) {
return request8081({
baseUrl: 'http://193.112.94.36:8081',
url: `/mall/brand/getTree/${storeId}`,
method: 'get',
params: { brandName }
})
}
// 删除品牌
export function deleteBrand(brandId) {
return request8081({
baseUrl: 'http://193.112.94.36:8081',
url: `/mall/brand/delete/${brandId}`,
method: 'delete'
})
}
// 修改品牌名称
export function updateBrand(data) {
return request8081({
baseUrl: 'http://193.112.94.36:8081',
url: '/mall/brand/update',
method: 'post',
data: data
})
}
// 新增分类
export function addClassification(data) {
return request8081({
baseUrl: 'http://193.112.94.36:8081',
url: '/mall/classification/add',
method: 'post',
data: data
})
}
// 获取分类树状结构
export function getClassificationTree(storeId, classificationName) {
return request8081({
baseUrl: 'http://193.112.94.36:8081',
url: `/mall/classification/getTree/${storeId}`,
method: 'get',
params: { classificationName }
})
}
// 修改分类名称
export function updateClassification(data) {
return request8081({
baseUrl: 'http://193.112.94.36:8081',
url: '/mall/classification/update',
method: 'post',
data: data
})
}
// 删除分类
export function deleteClassification(classificationId) {
return request8081({
baseUrl: 'http://193.112.94.36:8081',
url: `/mall/classification/delete/${classificationId}`,
method: 'delete'
})
}

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'
})
}

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

@ -0,0 +1,110 @@
import config from '@/config'
import storage from '@/utils/storage'
import constant from '@/utils/constant'
import { isHttp, isEmpty } from "@/utils/validate"
import { login, logout, getInfo } from '@/api/login'
import { getToken, setToken, removeToken } from '@/utils/auth'
import defAva from '@/static/images/profile.jpg'
const baseUrl = config.baseUrl
const user = {
state: {
token: getToken(),
id: storage.get(constant.id),
name: storage.get(constant.name),
avatar: storage.get(constant.avatar),
roles: storage.get(constant.roles),
permissions: storage.get(constant.permissions)
},
mutations: {
SET_TOKEN: (state, token) => {
state.token = token
},
SET_ID: (state, id) => {
state.id = id
storage.set(constant.id, id)
},
SET_NAME: (state, name) => {
state.name = name
storage.set(constant.name, name)
},
SET_AVATAR: (state, avatar) => {
state.avatar = avatar
storage.set(constant.avatar, avatar)
},
SET_ROLES: (state, roles) => {
state.roles = roles
storage.set(constant.roles, roles)
},
SET_PERMISSIONS: (state, permissions) => {
state.permissions = permissions
storage.set(constant.permissions, permissions)
}
},
actions: {
// 登录
Login({ commit }, userInfo) {
const username = userInfo.username.trim()
const password = userInfo.password
const code = userInfo.code
const uuid = userInfo.uuid
return new Promise((resolve, reject) => {
login(username, password, code, uuid).then(res => {
setToken(res.token)
commit('SET_TOKEN', res.token)
resolve()
}).catch(error => {
reject(error)
})
})
},
// 获取用户信息
GetInfo({ commit, state }) {
return new Promise((resolve, reject) => {
getInfo().then(res => {
const user = res.user
let avatar = (isEmpty(user) || isEmpty(user.avatar)) ? "" : user.avatar
if (!isHttp(avatar)) {
avatar = (isEmpty(avatar)) ? defAva : baseUrl + avatar
}
const userid = (isEmpty(user) || isEmpty(user.userId)) ? "" : user.userId
const username = (isEmpty(user) || isEmpty(user.userName)) ? "" : user.userName
if (res.roles && res.roles.length > 0) {
commit('SET_ROLES', res.roles)
commit('SET_PERMISSIONS', res.permissions)
} else {
commit('SET_ROLES', ['ROLE_DEFAULT'])
}
commit('SET_ID', userid)
commit('SET_NAME', username)
commit('SET_AVATAR', avatar)
resolve(res)
}).catch(error => {
reject(error)
})
})
},
// 退出系统
LogOut({ commit, state }) {
return new Promise((resolve, reject) => {
logout(state.token).then(() => {
commit('SET_TOKEN', '')
commit('SET_ROLES', [])
commit('SET_PERMISSIONS', [])
removeToken()
storage.clean()
resolve()
}).catch(error => {
reject(error)
})
})
}
}
}
export default user

61
ci-script.js Normal file
View File

@ -0,0 +1,61 @@
const ci = require('miniprogram-ci');
const path = require('path');
const fs = require('fs');
// 配置项(必须改!)
const config = {
appid: 'wx9393bde462d9805a', // 替换成你的正式小程序AppID
// 替换成你的密钥文件路径(比如 private.wx9393bde462d9805a.key
privateKeyPath: path.resolve(__dirname, './private.wx9393bde462d9805a.key'),
projectPath: path.resolve(__dirname, './unpackage/dist/dev/mp-weixin'),
version: '1.0.0',
desc: '自动化上传测试',
// ci-script.js 中的 setting 配置
setting: {
es6: true,
es7: true,
minify: true, // 基础压缩
minifyJS: true, // 压缩JS移除注释、空格、变量名混淆
minifyWXSS: true, // 压缩样式(合并重复样式、移除空格)
minifyXML: true, // 压缩配置文件app.json/page.json
autoPrefixWXSS: true,
codeProtect: false, // 关闭代码保护(保护会增加体积)
ignoreUnusedFiles: true, // 自动忽略未引用的文件
disableUseStrict: true, // 禁用"use strict"减少JS体积
disableShowSourceMap: true // 关闭sourcemap调试文件占体积
},
previewQrOutputPath: path.resolve(__dirname, './preview-qr.png'),
onProgressUpdate: (res) => {
console.log(`进度:${res.progress}%,状态:${res.status}`);
}
};
// 上传代码方法
async function uploadCode() {
try {
if (!fs.existsSync(config.privateKeyPath)) {
throw new Error(`密钥文件不存在:${config.privateKeyPath}`);
}
const project = new ci.Project({
appid: config.appid,
type: 'miniProgram',
projectPath: config.projectPath,
privateKeyPath: config.privateKeyPath,
ignores: ['node_modules/**/*'],
});
const uploadResult = await ci.upload({
project,
version: config.version,
desc: config.desc,
setting: config.setting,
onProgressUpdate: config.onProgressUpdate,
});
console.log('✅ 代码上传成功:', uploadResult);
} catch (error) {
console.error('❌ 上传失败:', error.message);
process.exit(1);
}
}
// 执行上传
uploadCode();

View File

@ -0,0 +1,269 @@
<template>
<view class="container">
<!-- 顶部导航栏 -->
<view class="navbar">
<view class="back-btn" @click="goBack">
<text class="back-icon"></text>
</view>
<view class="title">选择品牌</view>
</view>
<!-- 搜索框 -->
<view class="search-box">
<input type="text" placeholder="搜索品牌名称" class="search-input" v-model="searchKeyword" />
</view>
<!-- 品牌树状列表 -->
<view class="brand-tree">
<!-- 一级品牌项 -->
<view v-for="(brand, index) in filteredBrandList" :key="index" class="level1-item">
<view class="level1-header">
<text class="expand-icon" @click="toggleExpand(index)">{{ brand.expanded ? '' : '' }}</text>
<text class="level1-name" @click="toggleExpand(index)">{{ brand.name }}</text>
<view class="action-buttons">
<!-- 选择一级品牌按钮 -->
<text class="select-btn" @click.stop="selectBrand(brand.name, brand.id, null)">选择</text>
</view>
</view>
<!-- 二级子品牌容器 -->
<view v-if="brand.expanded && brand.children && brand.children.length > 0" class="level2-container">
<!-- 二级品牌项 -->
<view v-for="(subBrand, subIndex) in brand.children" :key="subIndex" class="level2-item">
<text class="level2-name">{{ subBrand.name }}</text>
<text class="select-btn" @click.stop="selectBrand(brand.name, brand.id, subBrand.name, subBrand.id)">选择</text>
</view>
</view>
</view>
</view>
<!-- 底部添加按钮 -->
<view class="add-brand-btn" @click="goToAddBrand">
<text class="add-btn-icon">+</text>
<text>添加品牌</text>
</view>
</view>
</template>
<script>
import { getBrandTree } from '@/api/product'
import { getStoreId } from '@/utils/auth'
export default {
data() {
return {
brandList: [],
searchKeyword: '',
storeId: null
}
},
computed: {
//
filteredBrandList() {
if (!this.searchKeyword.trim()) {
return this.brandList
}
const keyword = this.searchKeyword.toLowerCase().trim()
return this.brandList.filter(brand => {
//
const matchesLevel1 = brand.name.toLowerCase().includes(keyword)
//
const matchesLevel2 = brand.children && brand.children.some(subBrand => subBrand.name.toLowerCase().includes(keyword))
return matchesLevel1 || matchesLevel2
})
}
},
onLoad() {
// ID
this.storeId = getStoreId()
//
this.loadBrandList()
},
methods: {
//
async loadBrandList() {
try {
const res = await getBrandTree()
if (res.code === 200) {
// expandedtrue
this.brandList = res.data.map(brand => ({
...brand,
expanded: true,
children: brand.children || []
}))
}
} catch (error) {
console.error('获取品牌列表失败:', error)
}
},
// /
toggleExpand(index) {
this.brandList[index].expanded = !this.brandList[index].expanded
},
//
selectBrand(brandName, brandId, subBrandName, subBrandId) {
//
const selectedBrand = {
brandName: subBrandName || brandName,
brandId: subBrandId || brandId,
parentBrandName: subBrandName ? brandName : null,
parentBrandId: subBrandName ? brandId : null
}
//
uni.navigateBack({
delta: 1,
success: () => {
//
uni.$emit('brandSelected', selectedBrand)
}
})
},
//
goBack() {
uni.navigateBack({ delta: 1 })
},
//
goToAddBrand() {
uni.navigateTo({
url: '/pages/addBrand/addBrand'
})
}
}
}
</script>
<style scoped>
.container {
background-color: #f5f5f5;
min-height: 100vh;
display: flex;
flex-direction: column;
}
/* 导航栏 */
.navbar {
background-color: #e62318;
color: #fff;
display: flex;
align-items: center;
justify-content: space-between;
padding: 20rpx 30rpx;
position: relative;
height: 88rpx;
box-sizing: border-box;
}
.title {
font-size: 34rpx;
font-weight: bold;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
.back-icon {
font-size: 32rpx;
}
/* 搜索框 */
.search-box {
background-color: #fff;
padding: 16rpx 30rpx;
}
.search-input {
background-color: #f5f5f5;
border-radius: 6rpx;
padding: 18rpx;
font-size: 28rpx;
width: 100%;
box-sizing: border-box;
}
/* 品牌树 */
.brand-tree {
flex: 1;
padding: 20rpx 30rpx;
}
.level1-item {
background-color: #fff;
border-radius: 8rpx;
margin-bottom: 20rpx;
overflow: hidden;
}
.level1-header {
display: flex;
align-items: center;
padding: 20rpx 30rpx;
border-bottom: 1rpx solid #f0f0f0;
}
.expand-icon {
font-size: 24rpx;
margin-right: 15rpx;
color: #666;
}
.level1-name {
font-size: 28rpx;
flex: 1;
}
.action-buttons {
display: flex;
gap: 16rpx;
}
.select-btn {
background-color: #e62318;
color: #fff;
font-size: 24rpx;
padding: 10rpx 20rpx;
border-radius: 6rpx;
}
/* 二级子品牌容器 */
.level2-container {
display: flex;
padding: 24rpx 30rpx;
gap: 24rpx;
flex-wrap: wrap;
background-color: #fff;
}
.level2-item {
background-color: #f5f5f5;
border-radius: 8rpx;
width: 180rpx;
height: 180rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 16rpx;
}
.level2-name {
font-size: 28rpx;
color: #333;
}
/* 底部添加按钮 */
.add-brand-btn {
background-color: #e62318;
color: #fff;
display: flex;
align-items: center;
justify-content: center;
padding: 24rpx;
font-size: 30rpx;
margin: 0 30rpx 30rpx 30rpx;
border-radius: 8rpx;
}
.add-btn-icon {
margin-right: 8rpx;
}
</style>

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>

View File

@ -2,7 +2,10 @@
module.exports = {
// baseUrl: 'https://vue.ruoyi.vip/prod-api',
// baseUrl:'http://yunzs2.haich.cc',
// baseUrl:'https://yunzs.haich.cc',
baseUrl:'http://193.112.94.36:8080',
// baseUrl = 'https://api.ruoyi.com'
// prodApi: 'https://vue.ruoyi.vip/prod-api',
// baseUrl: 'http://localhost:8080',
// 应用信息
@ -12,7 +15,8 @@ module.exports = {
// 应用版本
version: "1.2.0",
// 应用logo
logo: "/static/logo.png",
logo: "http://193.112.94.36:8099/static/images/logo.png",
// logo: "http://yunzs.haich.cc/static/images/logo.png",
// 官方网站
site_url: "http://ruoyi.vip",
// 政策协议

View File

@ -21,21 +21,19 @@
"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\"/>"
"<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
"",
"<!-- 网络相关(仅当扫码后需要联网查商品时保留) -->",
"<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
"",
"<!-- 基础必要权限(如无特殊需求可保留) -->",
"<uses-permission android:name=\"android.permission.VIBRATE\"/>",
"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>"
]
},
"ios" : {
@ -50,7 +48,7 @@
},
"quickapp" : {},
"mp-weixin" : {
"appid" : "wxccd7e2a0911b3397",
"appid" : "wx29a83645aa2b4ebd",
"setting" : {
"urlCheck" : false,
"es6" : false,
@ -60,11 +58,17 @@
"optimization" : {
"subPackages" : true
},
"usingComponents" : true
"usingComponents" : true,
"networkTimeout" : {
"request" : 10000,
"connectSocket" : 10000,
"uploadFile" : 10000,
"downloadFile" : 10000
}
},
"vueVersion" : "2",
"h5" : {
"template" : "static/index.html",
"template" : "http://193.112.94.36:8099/static/images/index.html",
"devServer" : {
"port" : 9090,
"https" : false

5
package.json Normal file
View File

@ -0,0 +1,5 @@
{
"devDependencies": {
"miniprogram-ci": "^2.1.26"
}
}

212
pages.json Normal file
View File

@ -0,0 +1,212 @@
{
"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/product/product",
"style": {
"navigationBarTitleText": ""
}
}
],
"subPackages": [
{
"root": "pages/mine",
"pages": [
{"path": "avatar/index",
"style": {
"navigationBarTitleText": "修改头像"
}
},
{"path": "info/index",
"style": {
"navigationBarTitleText": "个人信息"
}
},
{"path": "info/edit",
"style": {
"navigationBarTitleText": "编辑资料"
}
},
{"path": "pwd/index",
"style": {
"navigationBarTitleText": "修改密码"
}
},
{"path": "setting/index",
"style": {
"navigationBarTitleText": "应用设置"
}
},
{"path": "help/index",
"style": {
"navigationBarTitleText": "常见问题"
}
},
{"path": "about/index",
"style": {
"navigationBarTitleText": "关于我们"
}
}
]
},
{
"root": "pages/common",
"pages": [
{"path": "webview/index",
"style": {
"navigationBarTitleText": "浏览网页"
}
},
{
"path": "textview/index",
"style": {
"navigationBarTitleText": "浏览文本"
}
}
]
},
{
"root": "pages/product",
"pages": [
{"path": "../addProduct/addProduct",
"style": {
"navigationBarTitleText": ""
}
},
{
"path": "../batchDeleteProduct/batchDeleteProduct",
"style": {
"navigationBarTitleText": ""
}
},
{
"path": "../edit/edit",
"style": {
"navigationBarTitleText": ""
}
}
]
},
{
"root": "pages/brand",
"pages": [
{
"path": "../addBrand/addBrand",
"style": {
"navigationBarTitleText": ""
}
},
{
"path": "../BrandSelector/BrandSelector",
"style": {
"navigationBarTitleText": "选择品牌"
}
}
]
},
{
"root": "pages/other",
"pages": [
{"path": "../user/user",
"style": {
"navigationBarTitleText": ""
}
},
{"path": "../settings/settings",
"style": {
"navigationBarTitleText": ""
}
},
{"path": "../asset/asset",
"style": {
"navigationBarTitleText": ""
}
},
{"path": "../enter/enter",
"style": {
"navigationBarTitleText": ""
}
},
{"path": "../storeSelect/storeSelect",
"style": {
"navigationBarTitleText": "选择门店"
}
},
{"path": "../import/import",
"style": {
"navigationBarTitleText": ""
}
},
{
"path": "../userStores/userStores",
"style": {
"navigationBarTitleText": ""
}
},
{
"path": "../category/category",
"style": {
"navigationBarTitleText": ""
}
},
{
"path": "../back/back",
"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"
}
}

View File

@ -0,0 +1,223 @@
<template>
<view v-if="visible" class="brand-selector-overlay" @click="handleClose">
<view class="brand-selector-container" @click.stop>
<!-- 头部 -->
<view class="selector-header">
<view class="header-left" @click="handleClose">
<text class="close-icon">×</text>
</view>
<view class="header-title">选择品牌</view>
<view class="header-right" @click="handleConfirm">
<text class="confirm-btn">完成</text>
</view>
</view>
<!-- 搜索框增加层级隔离+样式重置 -->
<view class="search-bar">
<view class="search-input">
<uni-icons class="search-icon" type="search" size="20" color="#CCCCCC"></uni-icons>
<input
type="text"
placeholder="搜索品牌名称"
placeholder-class="input-placeholder"
v-model="searchKeyword"
class="input"
@input="handleSearchInput"
/>
</view>
</view>
<!-- 品牌列表 -->
<view class="brand-list">
<view
class="brand-item"
:class="{active: selectedBrand === item.name}"
v-for="item in filteredBrandList"
:key="item.name"
@click="selectBrand(item.name)"
>
{{ item.name }}
</view>
</view>
<!-- 品牌管理入口 -->
<view class="brand-management" @click="handleToBrandManagement">
<text class="management-text">品牌管理</text>
</view>
</view>
</view>
</template>
<script>
export default {
name: 'BrandSelector',
props: {
visible: { type: Boolean, default: false },
defaultValue: { type: String, default: '默认品牌' },
brandList: { type: Array, default: () => [{ name: '默认品牌' }] }
},
data() {
return {
selectedBrand: this.defaultValue,
searchKeyword: ''
}
},
watch: {
defaultValue(newVal) {
this.selectedBrand = newVal
}
},
computed: {
filteredBrandList() {
if (!this.searchKeyword) return this.brandList
return this.brandList.filter(item =>
item.name.toLowerCase().includes(this.searchKeyword.toLowerCase())
)
}
},
methods: {
selectBrand(brandName) {
this.selectedBrand = brandName
},
handleConfirm() {
this.$emit('confirm', this.selectedBrand)
this.handleClose()
},
handleClose() {
this.$emit('close')
},
handleToBrandManagement() {
this.$emit('toBrandManagement')
},
handleSearchInput(e) {
this.searchKeyword = e.detail.value
}
}
}
</script>
<style lang="scss" scoped>
//
.brand-selector-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: flex-end;
z-index: 999;
}
//
.brand-selector-container {
width: 100%;
background: #FFFFFF;
border-radius: 16rpx 16rpx 0 0;
max-height: 80vh;
display: flex;
flex-direction: column;
z-index: 1000;
}
//
.selector-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 24rpx 32rpx;
border-bottom: 1rpx solid #F5F5F5;
.close-icon { font-size: 36rpx; color: #666666; font-weight: 400; }
.header-title { font-size: 32rpx; font-weight: 500; color: #333333; }
.confirm-btn { font-size: 30rpx; color: #E62429; font-weight: 400; }
}
// +
.search-bar {
padding: 20rpx 32rpx;
background: #FFFFFF;
position: relative;
z-index: 1001; //
.search-input {
display: flex;
align-items: center;
background: #FFFFFF !important;
border: 1rpx solid #E8E8E8;
border-radius: 8rpx;
height: 76rpx;
padding: 0 24rpx;
position: relative;
z-index: 1002; //
.search-icon {
margin-right: 16rpx;
color: #CCCCCC !important;
flex-shrink: 0;
position: relative;
z-index: 1; //
}
.input {
flex: 1;
font-size: 28rpx;
color: #333333 !important; //
border: none !important;
outline: none !important;
background: transparent !important;
padding: 0; //
line-height: 76rpx; //
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
position: relative;
z-index: 2; //
}
.input-placeholder {
font-size: 28rpx;
color: #CCCCCC !important;
}
}
}
//
.brand-list {
flex: 1;
overflow-y: auto;
.brand-item {
padding: 28rpx 32rpx;
border-bottom: 1rpx solid #F5F5F5;
font-size: 30rpx;
color: #333333;
position: relative;
font-weight: 400;
&.active {
color: #E62429;
&::after {
content: "✓";
position: absolute;
right: 32rpx;
top: 50%;
transform: translateY(-50%);
font-size: 28rpx;
font-weight: 600;
}
}
}
}
//
.brand-management {
padding: 28rpx 32rpx;
text-align: center;
color: #1677FF;
font-size: 28rpx;
border-top: 1rpx solid #F5F5F5;
font-weight: 400;
}
</style>

977
pages/addBrand/addBrand.vue Normal file
View File

@ -0,0 +1,977 @@
<template>
<view class="container">
<!-- 顶部导航栏 -->
<view class="navbar">
<view class="back-btn" @click="goBack">
<!-- <text class="back-icon"></text> -->
</view>
<view class="title">品牌管理</view>
</view>
<!-- 搜索框 -->
<view class="search-box">
<input
v-model="searchKeyword"
type="text"
placeholder="搜索品牌名称"
class="search-input"
@input="onSearchInput"
/>
</view>
<!-- 品牌树状列表 -->
<view class="brand-tree">
<!-- 一级品牌项 -->
<view v-for="(brand, index) in brandList" :key="index" class="level1-item">
<view class="level1-header">
<text class="expand-icon" @click="toggleExpand(index)">{{ brand.expanded ? '' : '' }}</text>
<text class="level1-name" @click="toggleExpand(index)">{{ brand.name }}</text>
<view class="menu-container" style="position: relative;">
<view class="menu-btn" @click.stop="toggleMenuOptions(index)">
<text class="menu-icon">···</text>
</view>
<!-- 菜单选项列表 -->
<view v-if="showMenuOptions && selectedBrandIndex === index" class="menu-options">
<!-- 重命名选项 -->
<view class="menu-option" @click.stop="showRenameDialog(index)">
<text class="rename-icon"></text>
<text>重命名</text>
</view>
<!-- 删除品牌选项 -->
<view class="menu-option delete-option" @click.stop="showDeleteBrandConfirm(index)">
<text class="trash-icon">🗑</text>
<text>删除品牌</text>
</view>
</view>
</view>
</view>
<!-- 二级子品牌列表展开时显示 -->
<view v-if="brand.expanded" class="level2-container">
<view
v-for="(subBrand, subIndex) in brand.children"
:key="subIndex"
class="level2-item"
draggable="true"
@dragstart="dragStart(index, subIndex)"
@dragover="dragOver"
@drop="drop(index, subIndex)"
>
<text class="level2-name">{{ subBrand.name }}</text>
<view class="edit-btn" @click.stop="openEditDialog(index, subIndex)">
<text class="edit-icon"></text>
<text class="edit-text">编辑</text>
</view>
</view>
<!-- 添加子品牌按钮 -->
<view class="add-sub-brand" @click="addSubBrand(index)">
<text class="add-icon">+</text>
<text class="add-text">添加子品牌</text>
</view>
</view>
</view>
</view>
<!-- 底部添加一级品牌按钮 -->
<view class="add-brand-btn" @click="addBrand">
<text class="add-btn-icon">+</text>
<text class="add-btn-text">添加品牌</text>
</view>
<!-- 修改品牌名称弹窗 -->
<view v-if="editDialogVisible" class="dialog-overlay">
<view class="dialog">
<view class="dialog-title">修改品牌名称</view>
<!-- 输入框 -->
<view class="input-wrapper">
<input
v-model="editBrandName"
type="text"
class="dialog-input"
placeholder="请输入品牌名称"
:focus="inputFocus"
@blur="inputFocus = false"
@focus="onInputFocus"
/>
<text v-if="editBrandName" class="clear-icon" @click="clearInput">×</text>
</view>
<!-- 带垃圾桶图标的删除按钮 -->
<view class="delete-btn" @click="showDeleteConfirm">
<text class="trash-icon">🗑</text>
<text>删除品牌</text>
</view>
<view class="dialog-buttons">
<view class="cancel-btn" @click="closeEditDialog"></view>
<view class="confirm-btn" @click="confirmEdit"></view>
</view>
</view>
</view>
<!-- 添加品牌弹窗 -->
<view v-if="addBrandDialogVisible" class="dialog-overlay">
<view class="dialog">
<view class="dialog-title">{{ isAddingSubBrand ? '添加子品牌' : '添加品牌' }}</view>
<!-- 输入框 -->
<view class="input-wrapper">
<input
v-model="addBrandName"
type="text"
class="dialog-input"
placeholder="请输入品牌名称"
:focus="inputFocus"
@blur="inputFocus = false"
@focus="onInputFocus"
/>
<text v-if="addBrandName" class="clear-icon" @click="clearAddInput">×</text>
</view>
<view class="dialog-buttons">
<view class="cancel-btn" @click="closeAddBrandDialog"></view>
<view class="confirm-btn" @click="confirmAddBrand"></view>
</view>
</view>
</view>
<!-- 删除确认弹窗在修改弹窗之上 -->
<view v-if="deleteConfirmVisible" class="delete-confirm-overlay">
<view class="delete-confirm-dialog">
<view class="delete-dialog-title">确认删除</view>
<view class="delete-dialog-content">确定要删除这个品牌吗</view>
<view class="delete-dialog-buttons">
<view class="delete-cancel-btn" @click="hideDeleteConfirm"></view>
<view class="delete-confirm-btn" @click="deleteBrand"></view>
</view>
</view>
</view>
</view>
</template>
<script>
// API
import { getBrandList, addBrand, getBrandTree, deleteBrand, updateBrand } from '@/api/product'
import { getStoreId } from '@/utils/auth'
export default {
data() {
return {
brandList: [],
storeId: null,
editDialogVisible: false,
deleteConfirmVisible: false,
editParentIndex: null,
editSubIndex: null,
editBrandName: '',
inputFocus: false,
dragSource: {
parentIndex: null,
subIndex: null
},
//
addBrandDialogVisible: false,
addBrandName: '',
isAddingSubBrand: false,
currentParentIndex: null,
//
showMenuOptions: false,
selectedBrandIndex: null,
isDeletingLevel1Brand: false,
//
renameDialogVisible: false,
renameBrandName: '',
renameBrandIndex: null,
//
editSubBrandId: null,
//
searchKeyword: ''
}
},
onLoad() {
// storeId
this.storeId = getStoreId()
//
this.loadBrandList()
},
methods: {
goBack() {
uni.navigateBack()
},
//
async loadBrandList(brandName = '') {
try {
// 使getBrandTreestoreIdURLbrandName
const res = await getBrandTree(this.storeId, brandName)
if (res.code === 200 && res.data) {
//
this.brandList = this.formatBrandData(res.data)
}
} catch (error) {
console.error('加载品牌列表失败:', error)
uni.showToast({ title: '加载品牌列表失败', icon: 'none' })
}
},
//
formatBrandData(brandData) {
// idbrandNamechildren
//
return brandData.map(brand => ({
name: brand.brandName,
id: brand.id,
expanded: true,
children: brand.children ? brand.children.map(subBrand => ({
name: subBrand.brandName,
id: subBrand.id
})) : []
}))
},
//
onSearchInput() {
const keyword = this.searchKeyword.trim()
// API
this.loadBrandList(keyword)
},
// /
toggleExpand(index) {
this.brandList[index].expanded = !this.brandList[index].expanded
},
//
addBrand() {
//
this.isAddingSubBrand = false
this.currentParentIndex = null
this.addBrandName = ''
this.addBrandDialogVisible = true
//
this.$nextTick(() => {
setTimeout(() => {
this.inputFocus = true
}, 50)
})
},
//
addSubBrand(parentIndex) {
//
this.isAddingSubBrand = true
this.currentParentIndex = parentIndex
this.addBrandName = ''
this.addBrandDialogVisible = true
//
this.$nextTick(() => {
setTimeout(() => {
this.inputFocus = true
}, 50)
})
},
//
async confirmAddBrand() {
if (!this.addBrandName.trim()) {
uni.showToast({ title: '请输入品牌名称', icon: 'none' })
return
}
try {
//
const params = {
brandName: this.addBrandName.trim(),
storeId: this.storeId
}
// parentId
if (this.isAddingSubBrand) {
params.parentId = this.brandList[this.currentParentIndex].id || 0
} else {
// parentId0
params.parentId = 0
}
//
const res = await addBrand(params)
if (res.code === 200) {
//
this.closeAddBrandDialog()
//
await this.loadBrandList()
uni.showToast({
title: '添加成功',
icon: 'success'
})
} else {
uni.showToast({ title: res.msg || '操作失败', icon: 'none' })
}
} catch (error) {
console.error('添加品牌失败:', error)
uni.showToast({ title: '添加失败', icon: 'none' })
}
},
//
closeAddBrandDialog() {
this.addBrandDialogVisible = false
this.addBrandName = ''
this.inputFocus = false
this.editSubBrandId = null
},
//
clearAddInput() {
this.addBrandName = ''
//
this.$nextTick(() => {
this.inputFocus = true
})
},
//
openEditDialog(parentIndex, subIndex) {
this.editParentIndex = parentIndex
this.editSubIndex = subIndex
//
if (subIndex === undefined) {
//
this.isDeletingLevel1Brand = true
this.editBrandName = this.brandList[parentIndex].name
} else {
//
this.isDeletingLevel1Brand = false
this.editBrandName = this.brandList[parentIndex].children[subIndex].name
}
this.editDialogVisible = true
this.$nextTick(() => {
setTimeout(() => {
this.inputFocus = true
}, 50)
})
},
//
onInputFocus() {
this.inputFocus = true
},
//
closeEditDialog() {
this.editDialogVisible = false
this.editParentIndex = null
this.editSubIndex = null
this.editBrandName = ''
this.inputFocus = false
},
//
clearInput() {
this.editBrandName = ''
setTimeout(() => {
this.inputFocus = true
}, 50)
},
//
async confirmEdit() {
if (this.editBrandName.trim() === '') {
uni.showToast({ title: '请输入品牌名称', icon: 'none' })
return
}
try {
//
let brandId;
//
if (this.isDeletingLevel1Brand) {
// 使id
brandId = this.brandList[this.editParentIndex].id
} else {
// 使id
brandId = this.brandList[this.editParentIndex].children[this.editSubIndex].id
}
const params = {
brandName: this.editBrandName.trim(),
id: brandId,
storeId: this.storeId
}
// API
const res = await updateBrand(params)
if (res.code === 200) {
//
if (this.isDeletingLevel1Brand) {
//
this.brandList[this.editParentIndex].name = this.editBrandName.trim()
} else {
//
this.brandList[this.editParentIndex].children[this.editSubIndex].name = this.editBrandName.trim()
}
//
this.closeEditDialog()
uni.showToast({ title: '修改成功', icon: 'success' })
} else {
uni.showToast({ title: res.msg || '修改失败', icon: 'none' })
}
} catch (error) {
console.error('修改品牌名称失败:', error)
uni.showToast({ title: '修改失败', icon: 'none' })
}
},
//
showDeleteConfirm() {
this.deleteConfirmVisible = true
},
//
hideDeleteConfirm() {
this.deleteConfirmVisible = false
},
//
async deleteBrand() {
try {
let brandId;
//
if (this.isDeletingLevel1Brand) {
// 使id
brandId = this.brandList[this.editParentIndex].id
} else {
// 使id
brandId = this.brandList[this.editParentIndex].children[this.editSubIndex].id
}
// API
const res = await deleteBrand(brandId)
if (res.code === 200) {
//
this.hideDeleteConfirm()
this.closeEditDialog()
//
await this.loadBrandList()
uni.showToast({ title: '删除成功', icon: 'success' })
} else {
uni.showToast({ title: res.msg || '删除失败', icon: 'none' })
}
} catch (error) {
console.error('删除品牌失败:', error)
uni.showToast({ title: '删除失败', icon: 'none' })
}
},
// /
toggleMenuOptions(index) {
if (this.showMenuOptions && this.selectedBrandIndex === index) {
//
this.showMenuOptions = false
this.selectedBrandIndex = null
} else {
//
this.showMenuOptions = true
this.selectedBrandIndex = index
}
},
//
showRenameDialog(index) {
//
this.showMenuOptions = false
this.selectedBrandIndex = null
// parentIndexsubIndex
this.openEditDialog(index)
},
//
showDeleteBrandConfirm(index) {
//
this.showMenuOptions = false
this.selectedBrandIndex = null
//
this.editParentIndex = index
this.editSubIndex = 0
this.isDeletingLevel1Brand = true
//
this.showDeleteConfirm()
},
//
dragStart(parentIndex, subIndex) {
this.dragSource = { parentIndex, subIndex }
},
//
dragOver(e) {
e.preventDefault()
},
//
drop(targetParentIndex, targetSubIndex) {
const { parentIndex, subIndex } = this.dragSource
if (parentIndex === targetParentIndex) {
const draggedItem = this.brandList[parentIndex].children.splice(subIndex, 1)[0]
this.brandList[targetParentIndex].children.splice(targetSubIndex, 0, draggedItem)
}
this.dragSource = { parentIndex: null, subIndex: null }
}
}
}
</script>
<style scoped>
.container {
background-color: #f5f5f5;
min-height: 100vh;
display: flex;
flex-direction: column;
}
/* 导航栏 */
.navbar {
background-color: #e62318;
color: #fff;
display: flex;
align-items: center;
justify-content: space-between;
padding: 20rpx 30rpx;
position: relative;
height: 88rpx;
box-sizing: border-box;
}
.title {
font-size: 34rpx;
font-weight: bold;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
.back-icon {
font-size: 32rpx;
}
.menu-icon {
font-size: 28rpx;
letter-spacing: 6rpx;
}
/* 菜单容器 */
.menu-container {
position: relative;
}
/* 菜单选项列表 */
.menu-options {
position: absolute;
top: 100%;
right: 0;
background-color: #fff;
border: 1rpx solid #f0f0f0;
border-radius: 8rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
z-index: 1000;
margin-top: 10rpx;
min-width: 200rpx;
}
/* 菜单项 */
.menu-option {
display: flex;
align-items: center;
gap: 10rpx;
font-size: 28rpx;
padding: 20rpx 30rpx;
cursor: pointer;
white-space: nowrap;
transition: background-color 0.2s;
}
/* 菜单项 hover 效果 */
.menu-option:active {
background-color: #f5f5f5;
}
/* 重命名选项 */
.menu-option:not(.delete-option) {
color: #333;
}
/* 重命名图标 */
.rename-icon {
font-size: 24rpx;
}
/* 删除品牌选项 */
.menu-option.delete-option {
color: #e62318;
border-top: 1rpx solid #f0f0f0;
}
/* 垃圾桶图标 */
.menu-option .trash-icon {
font-size: 24rpx;
}
/* 搜索框 */
.search-box {
background-color: #fff;
padding: 16rpx 30rpx;
}
.search-input {
background-color: #f5f5f5;
border-radius: 6rpx;
padding: 18rpx;
font-size: 28rpx;
width: 100%;
box-sizing: border-box;
color: #333;
height: 72rpx;
}
/* 品牌树 */
.brand-tree {
flex: 1;
padding: 20rpx 30rpx;
}
.level1-item {
background-color: #fff;
border-radius: 8rpx;
margin-bottom: 20rpx;
overflow: hidden;
}
.level1-header {
display: flex;
align-items: center;
padding: 20rpx 30rpx;
border-bottom: 1rpx solid #f0f0f0;
}
.expand-icon {
font-size: 24rpx;
margin-right: 15rpx;
color: #666;
}
.level1-name {
font-size: 28rpx;
flex: 1;
}
.more-btn {
font-size: 24rpx;
color: #999;
letter-spacing: 4rpx;
}
/* 二级子品牌容器 */
.level2-container {
display: flex;
padding: 24rpx 30rpx;
gap: 24rpx;
flex-wrap: wrap;
background-color: #fff;
}
.level2-item {
background-color: #f5f5f5;
border-radius: 8rpx;
width: 180rpx;
height: 180rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
cursor: move;
}
.level2-name {
font-size: 36rpx;
margin-bottom: 8rpx;
}
.edit-btn {
display: flex;
align-items: center;
font-size: 24rpx;
color: #999;
}
.edit-icon {
margin-right: 4rpx;
}
.add-sub-brand {
border: 2rpx dashed #1677ff;
border-radius: 8rpx;
width: 180rpx;
height: 180rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: #1677ff;
}
.add-icon {
font-size: 36rpx;
margin-bottom: 8rpx;
}
.add-text {
font-size: 24rpx;
}
/* 底部添加按钮 */
.add-brand-btn {
background-color: #e62318;
color: #fff;
display: flex;
align-items: center;
justify-content: center;
padding: 24rpx;
font-size: 30rpx;
margin: 0 30rpx 30rpx 30rpx;
border-radius: 8rpx;
}
.add-btn-icon {
margin-right: 8rpx;
}
/* ================ 弹窗样式 ================ */
.dialog-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.dialog {
background-color: #fff;
border-radius: 12rpx;
width: 85%;
max-width: 650rpx;
padding: 40rpx 50rpx;
box-sizing: border-box;
position: relative;
z-index: 1001;
}
.dialog-title {
font-size: 34rpx;
font-weight: 600;
text-align: center;
margin-bottom: 40rpx;
color: #333;
}
/* 输入框样式 */
.input-wrapper {
position: relative;
margin-bottom: 30rpx;
width: 100%;
}
.dialog-input {
border: 1rpx solid #e5e5e5;
border-radius: 8rpx;
padding: 22rpx 60rpx 22rpx 22rpx;
font-size: 30rpx;
width: 100%;
box-sizing: border-box;
outline: none;
background-color: #fff;
pointer-events: auto;
user-select: text;
-webkit-user-select: text;
caret-color: #e62318;
height: 88rpx;
line-height: normal;
}
.dialog-input:focus {
border-color: #e62318;
box-shadow: 0 0 0 2rpx rgba(230, 35, 24, 0.1);
}
.clear-icon {
position: absolute;
right: 20rpx;
top: 50%;
transform: translateY(-50%);
font-size: 28rpx;
color: #999;
width: 30rpx;
height: 30rpx;
text-align: center;
line-height: 30rpx;
border-radius: 50%;
background-color: #f5f5f5;
z-index: 1002;
cursor: pointer;
}
.clear-icon:active {
background-color: #e0e0e0;
}
/* 删除按钮 */
.delete-btn {
color: #e62318;
font-size: 28rpx;
text-align: center;
margin-bottom: 40rpx;
display: flex;
align-items: center;
justify-content: center;
gap: 8rpx;
padding: 16rpx;
border-radius: 8rpx;
/* background-color: #fff5f5; */
cursor: pointer;
}
.delete-btn:active {
background-color: #ffeaea;
}
.trash-icon {
font-size: 24rpx;
}
/* 弹窗按钮 */
.dialog-buttons {
display: flex;
gap: 24rpx;
}
.cancel-btn {
flex: 1;
background-color: #f5f5f5;
color: #666;
text-align: center;
padding: 24rpx;
border-radius: 8rpx;
font-size: 30rpx;
cursor: pointer;
}
.cancel-btn:active {
background-color: #e8e8e8;
}
.confirm-btn {
flex: 1;
background-color: #e62318;
color: #fff;
text-align: center;
padding: 24rpx;
border-radius: 8rpx;
font-size: 30rpx;
cursor: pointer;
}
.confirm-btn:active {
background-color: #d11f15;
}
/* ================ 删除确认弹窗(在修改弹窗之上) ================ */
.delete-confirm-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.7);
display: flex;
align-items: center;
justify-content: center;
z-index: 2000; /* 比修改弹窗更高的层级 */
}
.delete-confirm-dialog {
background-color: #fff;
border-radius: 16rpx;
width: 70%;
max-width: 560rpx;
padding: 40rpx;
box-sizing: border-box;
position: relative;
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: scale(0.9);
}
to {
opacity: 1;
transform: scale(1);
}
}
.delete-dialog-title {
font-size: 34rpx;
font-weight: 600;
text-align: center;
margin-bottom: 24rpx;
color: #333;
}
.delete-dialog-content {
font-size: 30rpx;
color: #666;
text-align: center;
margin-bottom: 40rpx;
line-height: 1.4;
}
.delete-dialog-buttons {
display: flex;
gap: 24rpx;
}
.delete-cancel-btn {
flex: 1;
background-color: #f5f5f5;
color: #333;
text-align: center;
padding: 24rpx;
border-radius: 8rpx;
font-size: 30rpx;
cursor: pointer;
}
.delete-cancel-btn:active {
background-color: #e8e8e8;
}
.delete-confirm-btn {
flex: 1;
background-color: #e62318;
color: #fff;
text-align: center;
padding: 24rpx;
border-radius: 8rpx;
font-size: 30rpx;
cursor: pointer;
}
.delete-confirm-btn:active {
background-color: #d11f15;
}
</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>

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

@ -0,0 +1,275 @@
<template>
<view class="page-wrapper">
<!-- 导航栏 -->
<view class="navbar">
<view class="nav-back" @click="goBack">
<!-- <text class="back-icon"></text> -->
</view>
<view class="nav-title">收入资产</view>
<view class="nav-refresh" @click="refresh">
<text class="refresh-icon"></text>
</view>
</view>
<!-- 资产卡片 -->
<view class="asset-card">
<view class="card-header">
<text class="card-title">我的资产</text>
</view>
<view class="asset-amount">0.00</view>
<view class="asset-stats">
<view class="stat-item">
<text class="stat-label">可提现</text>
<text class="stat-value">0.00</text>
</view>
<view class="stat-item">
<text class="stat-label">待结算</text>
<text class="stat-value">0.00</text>
<text class="stat-tip">次日可提现</text>
</view>
</view>
<view class="card-actions">
<button class="action-btn recharge-btn">充值</button>
<button class="action-btn withdraw-btn">提现到银行卡</button>
</view>
</view>
<!-- 资金明细 -->
<view class="fund-section">
<view class="section-title">资金明细</view>
<view class="detail-list">
<view class="detail-item active">
<text class="detail-text">可提现</text>
<text class="detail-amount">0.00 ></text>
</view>
<view class="detail-item">
<text class="detail-text">待结算</text>
<text class="detail-amount">0.00 ></text>
</view>
<view class="detail-item">
<text class="detail-text">已提现</text>
<text class="detail-amount">0.00 ></text>
</view>
<view class="detail-item">
<text class="detail-text">现金支付</text>
<text class="detail-amount">0.00 ></text>
</view>
</view>
</view>
<!-- 银行卡区域 -->
<view class="bank-section">
<view class="bank-item">
<text class="bank-text">我的银行卡</text>
<text class="bank-status">未绑定</text>
</view>
</view>
<!-- 常见问题 -->
<view class="faq-section">
<text class="faq-text">常见问题</text>
</view>
</view>
</template>
<script setup>
const goBack = () => {
uni.navigateBack()
}
const refresh = () => {
uni.showToast({
title: '刷新成功',
icon: 'none'
})
}
</script>
<style scoped>
/* 全局重置与基础样式 */
page {
background-color: #f5f5f5;
font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Helvetica Neue", Helvetica, Arial, sans-serif;
}
.page-wrapper {
min-height: 100vh;
background-color: #f5f5f5;
}
/* 状态栏 */
.status-bar {
display: flex;
justify-content: space-between;
padding: 10rpx 20rpx;
font-size: 24rpx;
color: #000;
background-color: #f5f5f5;
}
.status-icons {
display: flex;
align-items: center;
gap: 4rpx;
}
.icon {
font-size: 20rpx;
}
/* 导航栏 */
.navbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20rpx 30rpx;
background-color: #f5f5f5;
border-bottom: 1rpx solid #e5e5e5;
}
.nav-back {
width: 60rpx;
}
.back-icon {
font-size: 32rpx;
color: #333;
font-weight: 500;
}
.nav-title {
font-size: 34rpx;
font-weight: 500;
color: #333;
}
.nav-refresh {
width: 60rpx;
text-align: right;
}
.refresh-icon {
font-size: 28rpx;
color: #333;
}
/* 资产卡片 */
.asset-card {
margin: 20rpx 30rpx;
padding: 40rpx 30rpx 30rpx;
background-color: #fff;
border-radius: 16rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
text-align: center;
}
.card-header {
margin-bottom: 20rpx;
}
.card-title {
font-size: 28rpx;
color: #999;
}
.asset-amount {
font-size: 72rpx;
font-weight: 700;
color: #e63946;
margin-bottom: 40rpx;
}
.asset-stats {
display: flex;
justify-content: space-around;
margin-bottom: 40rpx;
}
.stat-item {
text-align: center;
}
.stat-label {
display: block;
font-size: 26rpx;
color: #999;
margin-bottom: 8rpx;
}
.stat-value {
font-size: 32rpx;
font-weight: 500;
color: #333;
}
.stat-tip {
display: block;
font-size: 22rpx;
color: #999;
margin-top: 4rpx;
}
.card-actions {
display: flex;
gap: 20rpx;
}
.action-btn {
flex: 1;
height: 80rpx;
border-radius: 8rpx;
font-size: 28rpx;
border: none;
background-color: #e9e9e9;
color: #999;
}
.withdraw-btn {
background-color: #1677ff;
color: #fff;
}
/* 资金明细 */
.fund-section {
margin: 0 30rpx 30rpx;
background-color: #fff;
border-radius: 16rpx;
overflow: hidden;
}
.section-title {
padding: 30rpx;
font-size: 30rpx;
font-weight: 500;
color: #333;
border-bottom: 1rpx solid #f0f0f0;
}
.detail-list {
padding: 0;
}
.detail-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 30rpx;
font-size: 28rpx;
color: #333;
border-bottom: 1rpx solid #f5f5f5;
}
.detail-item.active {
/* background-color: #f0f8ff; */
/* color: #1677ff; */
}
.detail-amount {
color: #999;
}
/* 银行卡区域 */
.bank-section {
margin: 0 30rpx 30rpx;
background-color: #fff;
border-radius: 16rpx;
}
.bank-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 30rpx;
font-size: 28rpx;
color: #333;
}
.bank-status {
color: #999;
}
/* 常见问题 */
.faq-section {
text-align: center;
padding: 20rpx;
}
.faq-text {
font-size: 26rpx;
color: #1677ff;
}
</style>

222
pages/back/back.vue Normal file
View File

@ -0,0 +1,222 @@
<template>
<view class="page-container">
<!-- 顶部状态栏模拟原生手机状态栏 -->
<!-- 导航栏 -->
<view class="navbar">
<view class="nav-tabs">
<view class="tab-item active">全部事件</view>
<view class="tab-item">只看订单</view>
</view>
</view>
<!-- 搜索与筛选区 -->
<view class="search-section">
<view class="search-box">
<text class="search-icon">🔍</text>
<input class="search-input" placeholder="稽查单/事件/订单/商品名/用户" />
<text class="split-icon"></text>
</view>
<view class="filter-btn">
<text class="filter-icon">🔍</text>
<text class="filter-text">筛选</text>
</view>
</view>
<!-- 内容区 -->
<view class="content">
<view class="empty-card">
<view class="empty-icon-wrapper">
<view class="empty-icon">📄</view>
</view>
<text class="empty-text">暂无数据</text>
</view>
</view>
</view>
</template>
<script>
export default {
methods: {
goBack() {
uni.navigateBack();
}
}
};
</script>
<style scoped>
/* 页面基础容器 */
.page-container {
width: 100%;
min-height: 100vh;
background-color: #f5f5f5;
display: flex;
flex-direction: column;
}
/* 状态栏 */
.status-bar {
height: 44px;
background-color: #e62318;
color: white;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 16px;
font-size: 17px;
font-weight: 500;
}
.status-icons {
display: flex;
gap: 8px;
}
/* 导航栏 */
.navbar {
height: 48px;
background-color: #e62318;
color: white;
display: flex;
align-items: center;
padding: 0 16px;
position: relative;
}
.nav-back {
width: 44px;
height: 44px;
display: flex;
align-items: center;
justify-content: center;
}
.back-icon {
font-size: 20px;
font-weight: 500;
}
.nav-tabs {
flex: 1;
display: flex;
justify-content: center;
gap: 40px;
}
.tab-item {
font-size: 17px;
padding: 0 8px;
position: relative;
}
.tab-item.active {
font-weight: bold;
}
.tab-item.active::after {
content: '';
position: absolute;
bottom: -8px;
left: 0;
right: 0;
height: 3px;
background-color: white;
border-radius: 2px;
}
/* 搜索与筛选区 */
.search-section {
background-color: white;
padding: 12px 16px;
display: flex;
align-items: center;
gap: 12px;
}
.search-box {
flex: 1;
height: 36px;
background-color: #f5f5f5;
border-radius: 6px;
border: 1px solid #e5e5e5;
display: flex;
align-items: center;
padding: 0 12px;
}
.search-icon {
font-size: 16px;
color: #999;
margin-right: 8px;
}
.search-input {
flex: 1;
font-size: 15px;
color: #333;
background: transparent;
border: none;
outline: none;
}
.split-icon {
font-size: 16px;
color: #999;
margin-left: 8px;
}
.filter-btn {
height: 36px;
background-color: #f5f5f5;
border-radius: 6px;
padding: 0 12px;
display: flex;
align-items: center;
gap: 4px;
font-size: 15px;
color: #333;
}
.filter-icon {
font-size: 16px;
}
/* 内容区 */
.content {
flex: 1;
padding: 24px 16px;
}
.empty-card {
background-color: white;
border-radius: 12px;
padding: 60px 0;
display: flex;
flex-direction: column;
align-items: center;
}
.empty-icon-wrapper {
width: 80px;
height: 80px;
background-color: #fff5f5;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 16px;
}
.empty-icon {
font-size: 32px;
color: #ffb3b3;
}
.empty-text {
font-size: 16px;
color: #666;
}
</style>

View File

@ -0,0 +1,380 @@
<template>
<view class="container">
<!-- 顶部导航栏 -->
<view class="navbar">
<view class="nav-back" @tap="goBack">
<uni-icons type="back" size="18" color="#fff"></uni-icons>
</view>
<view class="nav-title">批量删除</view>
<view class="nav-more">
<uni-icons type="more-filled" size="18" color="#fff"></uni-icons>
</view>
</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 class="filter-btn" @tap="showFilter = !showFilter">
<uni-icons type="filter" size="18" color="#999"></uni-icons>
<text>筛选</text>
</view>
</view>
<!-- 商品列表 -->
<scroll-view class="goods-list" scroll-y="true">
<!-- 空列表提示 -->
<view v-if="goodsList.length === 0" class="empty-tip">
<text>暂无商品数据</text>
</view>
<!-- 商品项 -->
<view
class="goods-item"
v-for="item in goodsList"
:key="item.id"
>
<view class="checkbox-wrap" @tap="toggleSelect(item.id)">
<view
class="custom-checkbox"
:class="{ checked: selectedIds.includes(String(item.id)) }"
>
<text v-if="selectedIds.includes(String(item.id))" class="check-icon"></text>
</view>
</view>
<view class="goods-info">
<image
:src="item.mainImage ? 'http://193.112.94.36:8081' + item.mainImage : '/static/default-goods.png'"
class="goods-img"
mode="aspectFill"
/>
<view class="goods-details">
<text class="goods-name">{{ item.productName || '未命名商品' }}</text>
<text class="goods-barcode">{{ item.productBarCode || '无条码' }}</text>
<text class="goods-price">{{ item.storePrice ? Number(item.storePrice).toFixed(2) : '0.00' }}</text>
</view>
</view>
</view>
</scroll-view>
<!-- 底部操作栏 -->
<view class="bottom-bar" v-if="goodsList.length > 0">
<view class="select-all" @tap="toggleSelectAll">
<view class="custom-checkbox" :class="{ checked: isAllSelected }">
<text v-if="isAllSelected" class="check-icon"></text>
</view>
<text>全选 (已选{{ selectedIds.length }})</text>
</view>
<button
class="delete-btn"
:class="{ disabled: selectedIds.length === 0 }"
:disabled="selectedIds.length === 0"
@tap="onDeleteSelected"
>
删除商品
</button>
</view>
</view>
</template>
<script>
import { getProductList as fetchProductList, batchDeleteProduct } from '@/api/product'
import { getToken, getStoreId } from '@/utils/auth'
export default {
data() {
return {
searchText: '',
showFilter: false,
storeId: null,
goodsList: [],
selectedIds: []
}
},
computed: {
allCheckboxValues() {
return this.goodsList.map(item => ({
text: '',
value: String(item.id)
}));
},
isAllSelected() {
return this.goodsList.length > 0 && this.selectedIds.length === this.goodsList.length;
}
},
onLoad() {
const storeId = getStoreId();
if (storeId) {
this.storeId = storeId;
this.getProductList();
} else {
uni.showToast({ title: '请先选择门店', icon: 'none', duration: 1500 });
setTimeout(() => uni.navigateBack(), 1500);
}
},
methods: {
goBack() {
uni.navigateBack();
},
//
async getProductList(searchParams = {}) {
try {
if (this.storeId) {
searchParams.storeId = this.storeId;
}
const res = await fetchProductList(searchParams);
if (res && res.code === 200 && Array.isArray(res.data)) {
this.goodsList = res.data;
this.selectedIds = [];
} else {
uni.showToast({ title: res?.msg || '获取商品失败', icon: 'none' });
}
} catch (error) {
console.error('获取商品异常:', error);
uni.showToast({ title: '网络错误', icon: 'none' });
}
},
//
onSearch() {
const keyword = this.searchText.trim();
if (!keyword) {
this.getProductList();
return;
}
const searchParams = /^\d+$/.test(keyword)
? { productBarCode: keyword }
: { productName: keyword };
this.getProductList(searchParams);
},
// /
toggleSelect(id) {
const idStr = String(id);
const index = this.selectedIds.indexOf(idStr);
if (index > -1) {
this.selectedIds.splice(index, 1);
} else {
this.selectedIds.push(idStr);
}
},
// /
toggleSelectAll() {
if (this.isAllSelected) {
this.selectedIds = [];
} else {
this.selectedIds = this.goodsList.map(item => String(item.id));
}
},
//
async onDeleteSelected() {
if (this.selectedIds.length === 0) return;
uni.showModal({
title: '确认删除',
content: `确定删除选中的${this.selectedIds.length}个商品?删除后不可恢复!`,
success: async (res) => {
if (res.confirm) {
try {
const idsToDelete = this.selectedIds.map(id => Number(id));
const result = await batchDeleteProduct(idsToDelete);
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' });
}
}
}
});
}
}
}
</script>
<style scoped>
/* 全局容器 */
.container {
background-color: #f5f5f5;
min-height: 100vh;
padding-bottom: 80rpx;
}
/* 导航栏 */
.navbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px;
background-color: #e63946;
color: #fff;
}
.nav-title { font-size: 18px; font-weight: 600; }
/* 搜索栏 */
.search-bar {
display: flex;
align-items: center;
padding: 12px 16px;
background-color: #fff;
}
.search-input {
flex: 1;
display: flex;
align-items: center;
background-color: #f5f5f5;
border-radius: 8px;
padding: 10px 12px;
margin-right: 12px;
}
.search-input input {
flex: 1;
margin-left: 8px;
font-size: 14px;
border: none;
outline: none;
}
.placeholder { color: #999; }
.filter-btn {
display: flex;
align-items: center;
font-size: 14px;
color: #666;
}
.filter-btn text { margin-left: 4px; }
/* 商品列表 */
.goods-list {
height: calc(100vh - 200px);
padding: 16px;
}
.empty-tip {
text-align: center;
padding: 40px 0;
color: #999;
font-size: 14px;
}
.goods-item {
display: flex;
align-items: center;
background-color: #fff;
border-radius: 8px;
padding: 12px;
margin-bottom: 12px;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
}
/* 复选框容器 */
.checkbox-wrap {
width: 80rpx;
height: 80rpx;
display: flex;
align-items: center;
justify-content: center;
margin-right: 12px;
}
/* 自定义复选框样式 */
.custom-checkbox {
width: 28rpx;
height: 28rpx;
border-radius: 4rpx;
border: 2rpx solid #ddd;
background-color: #fff;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
}
.custom-checkbox.checked {
background-color: #e63946;
border-color: #e63946;
}
.check-icon {
color: #fff;
font-size: 18rpx;
font-weight: bold;
}
/* 商品信息 */
.goods-info {
display: flex;
align-items: center;
flex: 1;
}
.goods-img {
width: 80rpx;
height: 80rpx;
border-radius: 8rpx;
margin-right: 12px;
}
.goods-details { flex: 1; }
.goods-name {
display: block;
font-size: 16px;
font-weight: 500;
color: #333;
margin-bottom: 4px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.goods-barcode {
font-size: 12px;
color: #999;
display: block;
margin-bottom: 4px;
}
.goods-price {
font-size: 12px;
color: #e63946;
font-weight: 600;
}
/* 底部操作栏 */
.bottom-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
background-color: #fff;
border-top: 1px solid #eee;
z-index: 99;
}
.select-all {
display: flex;
align-items: center;
font-size: 14px;
color: #333;
}
.select-all text { margin-left: 6px; }
.delete-btn {
padding: 10px 24px;
border-radius: 8px;
font-size: 16px;
background-color: #e63946;
color: #fff;
border: none;
}
.delete-btn.disabled {
background-color: #ccc;
color: #999;
cursor: not-allowed;
}
</style>

1070
pages/category/category.vue Normal file

File diff suppressed because it is too large Load Diff

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>

1485
pages/edit/edit.vue Normal file

File diff suppressed because it is too large Load Diff

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

@ -0,0 +1,635 @@
<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" @click="goNoCode">/</button>
</view>
<!-- 条码扫描区域 -->
<view class="scan-area">
<view class="scan-tip">对准商品条码自动识别</view>
<!-- App环境扫码 -->
<!-- #ifdef APP-PLUS -->
<view class="scan-view" id="barcode-view"></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>
<!-- #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="goods-list" v-if="goodsList.length > 0">
<view class="goods-item" v-for="(item, index) in goodsList" :key="index">
<view class="goods-info">
<image :src="item.image || '/static/default-goods.png'" class="goods-img"></image>
<view class="goods-detail">
<text class="goods-name">{{ item.name }}</text>
<text class="goods-barcode">{{ item.barcode }}</text>
<text class="goods-price">¥{{ item.price }}</text>
</view>
</view>
<view class="goods-actions">
<view class="action-btn remove" @click="removeGoods(index)"></view>
</view>
<view class="goods-form">
<view class="form-item">
<text class="label required">进货数量</text>
<input type="number" v-model="item.quantity" class="input" placeholder="请输入数量" />
</view>
<view class="form-item">
<text class="label">进货价</text>
<input type="number" v-model="item.price" class="input" placeholder="请输入价格" />
</view>
</view>
</view>
</view>
<!-- 空状态 -->
<view class="empty-area" v-else>
<text class="empty-title">暂未录入商品</text>
<text class="empty-desc">请在上方选择商品录入方式</text>
</view>
<!-- 提交按钮 -->
<view class="submit-area">
<button class="submit-btn" @click="submitList" :disabled="goodsList.length === 0">提交清单</button>
</view>
</view>
</template>
<script>
export default {
data() {
return {
flashOn: false,
scanPaused: false,
isScanning: false,
barcodeInstance: null,
goodsList: [
{
name: '怡宝饮用纯净水350ml',
barcode: '6901285991240',
price: 1,
quantity: 1,
image: 'http://193.112.94.36:8099/static/images/yibao.png'
}
]
};
},
mounted() {
// #ifdef APP-PLUS
console.log('页面 mounted 触发');
// Webview
this.$nextTick(() => {
setTimeout(() => {
this.initScan();
}, 300); // 300ms
});
// #endif
},
onShow() {
// #ifdef APP-PLUS
//
if (!this.barcodeInstance) {
this.$nextTick(() => {
setTimeout(() => {
this.requestCameraAuth().then(() => {
this.initBarcodeScan();
}).catch(err => {
console.log('权限不足,无法重新初始化');
});
}, 300);
});
}
// #endif
},
onUnload() {
this.destroyScan();
},
methods: {
goBack() {
this.destroyScan();
uni.navigateBack({ delta: 1 });
},
goImport() {
uni.navigateTo({ url: '/pages/import/import' });
},
goNoCode() {
uni.navigateTo({ url: '/pages/NoCode/NoCode' });
},
goRecord() {
uni.showToast({ title: '记录功能开发中', icon: 'none' });
},
initScan() {
this.requestCameraAuth().then(() => {
this.initBarcodeScan();
}).catch(err => {
console.error('权限获取失败:', err);
uni.showModal({
title: '权限提示',
content: '请前往设置开启相机权限',
confirmText: '去设置',
success: (res) => {
if (res.confirm) {
this.openAppSettings();
}
}
});
});
},
requestCameraAuth() {
return new Promise((resolve, reject) => {
// ... ...
// ()
const platform = uni.getSystemInfoSync().platform;
if (platform === 'android') {
const main = plus.android.runtimeMainActivity();
const Permission = plus.android.importClass('android.Manifest.permission');
const PackageManager = plus.android.importClass('android.content.pm.PackageManager');
if (main.checkSelfPermission(Permission.CAMERA) === PackageManager.PERMISSION_GRANTED) {
resolve();
} else {
plus.android.requestPermissions(['android.permission.CAMERA'], (res) => {
res[0].granted ? resolve() : reject('用户拒绝');
}, reject);
}
} else {
resolve();
}
});
},
openAppSettings() {
// ... ...
try {
plus.android.importClass('android.content.Intent');
plus.android.importClass('android.provider.Settings');
plus.android.importClass('android.net.Uri');
const mainActivity = plus.android.runtimeMainActivity();
const intent = new android.content.Intent();
intent.setAction(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
const uri = android.net.Uri.fromParts('package', mainActivity.getPackageName(), null);
intent.setData(uri);
intent.addFlags(android.content.Intent.FLAG_ACTIVITY_NEW_TASK);
mainActivity.startActivity(intent);
} catch (e) {
uni.showToast({ title: '跳转失败,请手动开启', icon: 'none' });
}
},
initBarcodeScan() {
if (!plus || !plus.barcode) return;
this.destroyScan();
this.isScanning = true;
const sysInfo = uni.getSystemInfoSync();
const statusBarHeight = sysInfo.statusBarHeight || 0;
const navBarHeight = 44;
const totalNavBarHeight = statusBarHeight + navBarHeight;
//
console.log('高度计算:', { statusBarHeight, navBarHeight, totalNavBarHeight });
//
const scanTop = totalNavBarHeight + 80;
const scanWidth = sysInfo.windowWidth - 30;
const scanHeight = 250; // 0
//
const barcode = plus.barcode.create('barcode',
[plus.barcode.CODE_128, plus.barcode.EAN_13, plus.barcode.EAN_8],
{
top: scanTop,
left: 15,
width: scanWidth,
height: scanHeight,
scanbarColor: '#e60012',
frameColor: '#e60012',
background: '#000',
zIndex: 9999 //
}
);
barcode.onmarked = (type, result) => {
if (result) {
console.log('扫码结果:', result);
this.addGoodsByBarcode(result);
}
};
// Webview
const currentWebview = this.$scope.$getAppWebview();
if (currentWebview) {
currentWebview.append(barcode);
console.log('扫码控件已挂载到 Webview');
} else {
console.error('获取 Webview 失败,无法挂载');
}
barcode.start();
this.barcodeInstance = barcode;
},
destroyScan() {
if (this.barcodeInstance) {
this.barcodeInstance.close();
this.barcodeInstance = null;
}
this.isScanning = false;
},
toggleFlash() {
if (this.barcodeInstance) {
this.flashOn = !this.flashOn;
this.barcodeInstance.setFlash(this.flashOn);
}
},
pauseScan() {
if (this.barcodeInstance) {
this.scanPaused = !this.scanPaused;
this.scanPaused ? this.barcodeInstance.pause() : this.barcodeInstance.resume();
}
},
addGoodsByBarcode(barcode) {
const mockGoods = {
name: '测试商品-' + barcode,
barcode: barcode,
price: 10,
quantity: 1,
image: 'http://193.112.94.36:8099/static/images/yibao.png'
};
this.goodsList.push(mockGoods);
uni.showToast({ title: '识别成功', icon: 'success' });
},
removeGoods(index) {
this.goodsList.splice(index, 1);
},
submitList() {
uni.showToast({ title: '提交成功', icon: 'success' });
setTimeout(() => uni.navigateBack(), 1000);
}
}
};
</script>
<style scoped>
/* 页面基础样式 */
.input-list-page {
width: 100%;
min-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: calc(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;
}
/* 扫码区域关键修复增加z-index */
.scan-area {
width: 100%;
padding: 15px;
box-sizing: border-box;
position: relative;
margin-top: 10px;
background-color: #fff;
z-index: 1; /* 确保扫码区域在最上层 */
}
.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;
z-index: 2; /* 关键修复:确保扫码容器层级 */
}
.scan-controls {
position: absolute;
bottom: 15px;
left: 0;
width: 100%;
display: flex;
justify-content: center;
gap: 40px;
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);
min-width: 60px;
}
.control-text {
font-size: 12px;
margin-top: 5px;
text-align: center;
}
/* H5端提示 */
.h5-tip {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 20px;
background-color: #f9f9f9;
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;
}
/* 商品列表 */
.goods-list {
flex: 1;
padding: 15px;
box-sizing: border-box;
}
.goods-item {
background-color: #fff;
border-radius: 8px;
padding: 15px;
margin-bottom: 10px;
}
.goods-info {
display: flex;
align-items: flex-start;
margin-bottom: 10px;
}
.goods-img {
width: 48px;
height: 48px;
border-radius: 4px;
margin-right: 10px;
}
.goods-detail {
flex: 1;
}
.goods-name {
font-size: 16px;
color: #333;
display: block;
margin-bottom: 4px;
}
.goods-barcode {
font-size: 12px;
color: #999;
display: block;
margin-bottom: 4px;
}
.goods-price {
font-size: 14px;
color: #e60012;
font-weight: bold;
}
.goods-actions {
display: flex;
justify-content: flex-end;
margin-bottom: 10px;
}
.action-btn {
padding: 6px 12px;
border-radius: 4px;
font-size: 12px;
cursor: pointer;
}
.remove {
background-color: #fef0f0;
color: #ff4d4f;
border: 1px solid #ffccc7;
}
/* 商品表单 */
.goods-form {
display: flex;
gap: 20px;
}
.form-item {
flex: 1;
display: flex;
flex-direction: column;
gap: 4px;
}
.label {
font-size: 14px;
color: #333;
}
.required::before {
content: '*';
color: #e60012;
margin-right: 2px;
}
.input {
height: 32px;
border: 1px solid #eee;
border-radius: 4px;
padding: 0 8px;
font-size: 14px;
}
/* 空状态 */
.empty-area {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 20px;
}
.empty-title {
font-size: 16px;
color: #333;
margin-bottom: 8px;
}
.empty-desc {
font-size: 14px;
color: #999;
}
/* 提交按钮区域 */
.submit-area {
padding: 15px;
box-sizing: border-box;
background-color: #fff;
}
.submit-btn {
width: 100%;
height: 44px;
background-color: #e60012;
color: #fff;
border: none;
border-radius: 4px;
font-size: 16px;
}
.submit-btn:disabled {
background-color: #ccc;
color: #999;
}
</style>

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>

1287
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>

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>

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

@ -0,0 +1,619 @@
<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
console.log('Avatar module base URL:', 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>

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

@ -0,0 +1,854 @@
<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" v-if="currentTab === 'all'">
<view class="status-content">
<view class="dui">
<image class="dui-img" src="http://193.112.94.36:8099/static/images/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>
<!-- 临期/过期状态栏 -->
<view class="store-status" v-else-if="currentTab === 'expiring'">
<view class="status-left-content">
<view class="dui">
<image class="dui-img" src="http://193.112.94.36:8099/static/images/Mask group.png"></image>
</view>
<view class="status-left">
<text class="status-text">快速处理</text>
<text class="status-num">临期/过期食品</text>
</view>
</view>
<view class="status-right-buttons">
<view class="quick-btn pink-btn" @tap="goToPromotion">
<text class="btn-text">临期促销</text>
<uni-icons type="arrowright" size="12" color="#fff"></uni-icons>
</view>
<view class="quick-btn pink-btn" @tap="goToExpiredOut">
<text class="btn-text">过期出库</text>
<uni-icons type="arrowright" size="12" color="#fff"></uni-icons>
</view>
</view>
</view>
<!-- 缺货状态栏 -->
<view class="store-status" v-else-if="currentTab === 'outstock'">
<view class="status-content">
<view class="dui">
<image class="dui-img" src="http://193.112.94.36:8099/static/images/Mask group (1).png"></image>
</view>
<view class="status-left">
<text class="status-text">库存不足预警设置</text>
<text class="status-num">库存低于已设置阈值时会收到APP推送提醒</text>
</view>
</view>
<view class="status-right" @tap="goToAlertSettings">
<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 : 'http://193.112.94.36:8099/static/images/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>
</scroll-view>
<!-- 底部操作按钮 -->
<view class="bottom-actions">
<view class="action-btn more-btn" @tap="showMoreActions()">
<text>更多</text>
</view>
<button class="action-button primary" @tap="addNewGoods">
添加商品/库存
</button>
</view>
<!-- 更多操作弹窗 -->
<uni-popup ref="popup" type="bottom">
<view class="popup-content">
<view class="popup-item" @tap="goToInventoryCount">
<uni-icons type="scan" size="20" color="#333"></uni-icons>
<text>盘点库存</text>
</view>
<view class="popup-item" @tap="goToOutbound">
<uni-icons type="arrowright" size="20" color="#333"></uni-icons>
<text>出库</text>
</view>
<view class="popup-item" @tap="batchDeleteGoods">
<uni-icons type="trash" size="20" color="#333"></uni-icons>
<text>批量删除商品</text>
</view>
<view class="popup-item" @tap="goToCategoryManagement">
<uni-icons type="apps" size="20" color="#333"></uni-icons>
<text>分类管理</text>
</view>
<view class="popup-item" @tap="goToBrandManagement">
<uni-icons type="navigate" size="20" color="#333"></uni-icons>
<text>品牌管理</text>
</view>
<view class="popup-item" @tap="goToBarcodeSettings">
<uni-icons type="settings" size="20" color="#333"></uni-icons>
<text>条码规则设置</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(options) {
// ID
const storeId = getStoreId();
if (storeId) {
this.storeId = storeId;
console.log('当前门店ID:', storeId);
} else {
uni.showToast({
title: '请先选择门店',
icon: 'none'
});
setTimeout(() => {
uni.navigateBack();
}, 1500);
return;
}
// tab
if (options && options.tab) {
this.currentTab = options.tab;
}
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;
}
//
if (this.currentTab === 'outstock') {
//
searchParams.stockQuantity = 0;
} else if (this.currentTab === 'expiring') {
// /
// API
// searchParams.expiring = true;
}
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 {
this.goodsList = [];
uni.showToast({
title: res?.msg || '获取商品列表失败',
icon: 'none'
});
}
} catch (error) {
this.goodsList = [];
console.error('获取商品列表失败:', error);
console.error('错误详情:', JSON.stringify(error));
uni.showToast({
title: '网络请求失败',
icon: 'none'
});
}
},
switchTab(tab) {
this.currentTab = tab;
//
this.getProductList();
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 = { productBarCode: searchText };
console.log('按条形码精准查询:', searchParams);
} else {
searchParams = { productName: searchText };
console.log('商品名称模糊查询:', searchParams);
}
this.getProductList(searchParams);
},
goToSettings() {
uni.navigateTo({
url: '/pages/settings/index'
});
},
//
goToAlertSettings() {
uni.navigateTo({
url: '/pages/alertSettings/index'
});
},
//
goToPromotion() {
console.log('临期促销');
//
},
//
goToExpiredOut() {
console.log('过期出库');
//
},
showMoreActions() {
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/addProduct/addProduct'
// url: '/pages/enter/enter'
});
},
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'
});
}
}
});
},
//
goToInventoryCount() {
this.$refs.popup.close();
uni.navigateTo({
url: '/pages/inventoryCount/inventoryCount'
});
},
//
goToOutbound() {
this.$refs.popup.close();
uni.navigateTo({
url: '/pages/outbound/outbound'
});
},
//
batchDeleteGoods() {
this.$refs.popup.close();
uni.navigateTo({
url: '/pages/batchDeleteProduct/batchDeleteProduct'
});
},
//
goToCategoryManagement() {
this.$refs.popup.close();
uni.navigateTo({
url: '/pages/category/category'
});
},
//
goToBrandManagement() {
this.$refs.popup.close();
uni.navigateTo({
url: '/pages/addBrand/addBrand'
});
},
//
goToBarcodeSettings() {
this.$refs.popup.close();
uni.navigateTo({
url: '/pages/barcodeSettings/barcodeSettings'
});
}
}
}
</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;
}
.status-left-content {
display: flex;
align-items: center;
}
.status-right-buttons {
display: flex;
align-items: center;
gap: 12rpx;
}
.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;
}
/* 右侧粉色按钮通用样式 */
.quick-btn {
display: flex;
align-items: center;
background: red;
padding: 8rpx 16rpx;
border-radius: 20rpx;
color: white;
font-size: 22rpx;
gap: 4rpx;
flex-shrink: 0;
}
.btn-text {
font-size: 22rpx;
}
/* 商品列表 */
.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>

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

@ -0,0 +1,204 @@
<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 class="logout-module">
<view class="logout-btn" @click="handleLogout">
<text class="logout-text">退出登录</text>
</view>
</view>
</view>
</template>
<script>
export default {
data() {
return {
//
}
},
methods: {
navigateTo(pageName) {
//
uni.showToast({
title: `跳转到${pageName}`,
icon: 'none',
duration: 1000
});
},
handleLogout() {
this.$modal.confirm('确定注销并退出系统吗?').then(() => {
this.$store.dispatch('LogOut').then(() => {}).finally(()=>{
this.$tab.reLaunch('/pages/index')
})
})
}
}
}
</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;
}
}
.logout-text {
font-size: 34rpx;
color: red;
}
.logout-btn {
width: 100%;
height: 88rpx;
background-color: #ffffff;
border-radius: 12rpx;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
}
/* ========== 退出登录模块 ========== */
.logout-module {
margin-top: 40rpx;
padding: 0 40rpx;
}
.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>

View File

@ -0,0 +1,160 @@
<template>
<view class="nickname-page">
<!-- 顶部导航栏 -->
<view class="navbar">
<view class="nav-left" @click="goBack">
<!-- <text class="nav-icon">&lt;</text> -->
</view>
<view class="nav-title">昵称</view>
<view class="nav-right" @click="saveNickname">
<text class="nav-text">完成</text>
</view>
</view>
<!-- 内容区 -->
<view class="content">
<view class="input-item">
<text class="label">名称</text>
<input
class="input"
v-model="nickname"
placeholder="请输入昵称"
maxlength="20"
/>
</view>
</view>
</view>
</template>
<script>
export default {
data() {
return {
nickname: '' //
}
},
onLoad(options) {
//
if (options.oldNickname) {
this.nickname = options.oldNickname
}
},
methods: {
//
goBack() {
uni.navigateBack()
},
//
saveNickname() {
if (!this.nickname.trim()) {
uni.showToast({
title: '昵称不能为空',
icon: 'none'
})
return
}
//
uni.showLoading({
title: '保存中...'
})
//
setTimeout(() => {
uni.hideLoading()
uni.showToast({
title: '保存成功',
icon: 'success'
})
//
uni.navigateBack({
success() {
const pages = getCurrentPages()
const prevPage = pages[pages.length - 1]
if (prevPage) {
//
prevPage.$vm.$emit('updateNickname', this.nickname)
}
}
})
}, 1000)
}
}
}
</script>
<style lang="scss" scoped>
.nickname-page {
min-height: 100vh;
background-color: #f5f5f5;
}
/* 顶部导航栏 */
.navbar {
display: flex;
align-items: center;
justify-content: space-between;
height: 44px;
background-color: #409eff;
color: #fff;
padding: 0 15px;
}
.nav-left {
width: 40px;
height: 44px;
display: flex;
align-items: center;
justify-content: flex-start;
}
.nav-icon {
font-size: 18px;
font-weight: bold;
}
.nav-title {
font-size: 17px;
font-weight: 500;
}
.nav-right {
width: 40px;
height: 44px;
display: flex;
align-items: center;
justify-content: flex-end;
}
.nav-text {
font-size: 16px;
}
/* 内容区 */
.content {
margin-top: 10px;
background-color: #fff;
}
.input-item {
display: flex;
align-items: center;
padding: 12px 15px;
border-bottom: 1px solid #eee;
}
.label {
font-size: 16px;
color: #333;
width: 60px;
flex-shrink: 0;
}
.input {
flex: 1;
font-size: 16px;
color: #333;
text-align: right;
}
</style>

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

@ -0,0 +1,331 @@
<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" @click="goUpdateUserName">
<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" @click="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'
})
},
goUpdateUserName(){
uni.navigateTo({
url: '/pages/updateUserName/updateUserName'
});
},
//
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)
}
})
}
}
})
},
handleLogout() {
this.$modal.confirm('确定注销并退出系统吗?').then(() => {
this.$store.dispatch('LogOut').then(() => {}).finally(()=>{
this.$tab.reLaunch('/pages/index')
})
})
}
}
}
</script>
<style>
.container {
background-color: #f5f5f5;
min-height: 100vh;
}
.touXiang{
width: 150rpx;
height: 150rpx;
border-radius: 50%;
background-color: #ffffff;
background-image: url('http://193.112.94.36:8099/static/images/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: firebrick;
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()
}
}

View File

@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEAwaAlslxygdY6X7jWg9l3py4TXn6Kdqnu2oA4cvqvYjekRxZG
UNllThCnD8f7nOwJVKHUvvCifC2xb34QXcxQSqLBN5zF1LaenAIEULcu/1p9l3Eq
KLaaa7IEaRQz1LY4R6nnAwbHoWFIF2Cvr3k6lHLWxG3Cs2oI/oF1NHPu8oap2Cgp
un+m4XX+1L9ylt1iTn3FEcT8MmaTXmqILCsFv2dXYZtZncBvIUP1FRsFzWqSt2Dm
jsSD0+OxFiCT4MaUXxe+XLH+eTKy28AhGhIP07DFctR11iyfYEsRjfgRBRuKEHG6
t+t9OdUhyB6E/BiFXzzJuuRhe7yS/SGvezQt9QIDAQABAoIBAQCUloZ3QtSo6LKx
RJJyak+VXxmEGY2+lJf03BL1wYUX1WVfHCvn3X0NlF/wD2L6wHREm1A9G0NGEnao
/dAneyReslmeiNOUcnRzemS/YGRTl6jrr+9PgRot7VXPIa7I3PGBpVPfkbNfF92P
+yW3fkvDIgHIigaxUn0GemhsUU+ckv9Ihy6kE+WkH5leqDEBOC2MnLpwZJLWqGh/
Bs4RkKaAV9FPeuAbny6nEL6KvSHBT1vUnyw/xCAUsH216TDZAbIpkjnA40vORykX
D/NIZECGywCvD2nTS5TCwb7ecVtBodB1GPNdNpqTaNONskbjIzClCZwwLWWf6nV6
Ws/ay+6hAoGBAOCpkN1c+uR48ypb+p2O6wY+xejuYYnAP6y908q+hGyc7cKSEmgL
/berEsrpR4ZYAC5QXphfGsxiYf6YH1vRngETJseydPP/aGI/LplPXQWLbmJropmX
zjqoIYuar8qQnpxCCKodU/geoIAE+okopUE2T3zEpMbxSqoQZ0JZKaW5AoGBANyi
Sxes9UoNEJ07Xk1zxVkxDWUYAbwlM9wUCEqAI8PhyEWi+s7KcXswDXQ++h/rQfCc
ezKZBxDmJR7Il2+vAJyQgk0yhHE7s1dQcI325wVeuYJ63lDgT3qcSS0DjfDail3U
y+dJPvo8c9oc5slj9GpKCDn7U1GkkofgX3bAE6gdAoGAOo63/ZrQomCMMQxMZGju
BXCzMSWBMuBzOFk6LOw/o/e7WS2tsoT9mrPycAUh6Xhig6/bGCgh2ggCttN7yPj4
EBunzgFLzpVR5dnGEZvICTvwh6K6fQI+dLeCFts42rmbPetQStbeHhwNhZDGpJ19
hWPckA7JTDl0VqNz5q1K17ECgYBdq8mV06iQN9vF5V60I2K16010jiyuZF0QIrEi
cCS/FSyh4//3q5tiYZRUtigbRRZJwSXM5YtKcWtxFli04eewkOnBPKFeMaqCd3RR
0XFjpkO8Uc3xKEqWE6Q9qDSq/R2hmKa5Gy/RrbjB8WNKPVWXirbTZxCIqQZNCcV9
9S5jQQKBgQCBjwzjox8weiS+UsrxC/fF0nQ6LgObzn2kqjtKq/kKBYfZ7CJ8pQ1x
lAq6C9cSeOi1PiGxHFiS5t61hNfPYQtlT2+sk11luSb6k+wfusNa1OCR7NYgZjP5
KPgOP4G503KPIHCmhumDwz4USy1DMw9JuekuP8jsnP7NyvU473C99g==
-----END RSA PRIVATE KEY-----

35
project.config.json Normal file
View File

@ -0,0 +1,35 @@
{
"setting": {
"es6": true,
"postcss": true,
"minified": true,
"uglifyFileName": false,
"enhance": true,
"packNpmRelationList": [],
"babelSetting": {
"ignore": [],
"disablePlugins": [],
"outputPath": ""
},
"useCompilerPlugins": false,
"minifyWXML": true,
"compileWorklet": false,
"uploadWithSourceMap": true,
"packNpmManually": false,
"minifyWXSS": true,
"localPlugins": false,
"disableUseStrict": false,
"condition": false,
"swc": false,
"disableSWC": true
},
"compileType": "miniprogram",
"simulatorPluginLibVersion": {},
"packOptions": {
"ignore": [],
"include": []
},
"appid": "wxe94413d023e0e7df",
"editorSetting": {},
"libVersion": "3.14.0"
}

View File

@ -0,0 +1,21 @@
{
"libVersion": "3.14.0",
"projectname": "RuoYi-App-master",
"setting": {
"urlCheck": false,
"coverView": true,
"lazyloadPlaceholderEnable": false,
"skylineRenderEnable": false,
"preloadBackgroundData": false,
"autoAudits": false,
"showShadowRootInWxmlPanel": true,
"compileHotReLoad": true,
"useApiHook": true,
"useStaticServer": false,
"useLanDebug": false,
"showES6CompileOption": false,
"checkInvalidKey": true,
"ignoreDevUnusedFiles": true,
"bigPackageSizeSupport": false
}
}

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.

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 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>

5142
static/scss/colorui.css Normal file

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,451 @@
/* 跨平台兼容样式文件 */
/* ==================== 基础重置 ==================== */
* {
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;
}
/* ==================== 平台检测 ==================== */
/* H5平台 */
/* #ifdef H5 */
page {
background-color: #f5f7fa;
}
/* #endif */
/* App平台 */
/* #ifdef APP-PLUS */
page {
background-color: #f5f7fa;
padding-top: var(--status-bar-height, 0px);
}
/* #endif */
/* 微信小程序 */
/* #ifdef MP-WEIXIN */
page {
background-color: #f5f7fa;
}
/* #endif */
/* ==================== 像素比处理 ==================== */
/* 高像素比设备Retina屏幕 */
@media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) {
.border-1px {
position: relative;
}
.border-1px::after {
content: '';
position: absolute;
top: 0;
left: 0;
width: 200%;
height: 200%;
border: 1px solid var(--border-color, #e0e0e0);
transform: scale(0.5);
transform-origin: 0 0;
pointer-events: none;
}
}
/* 超高像素比设备 */
@media (-webkit-min-device-pixel-ratio: 3), (min-resolution: 288dpi) {
.border-1px::after {
width: 300%;
height: 300%;
transform: scale(0.333);
}
}
/* ==================== 字体优化 ==================== */
/* 修复移动端字体模糊 */
.text-blur-fix {
-webkit-transform: translateZ(0);
transform: translateZ(0);
backface-visibility: hidden;
-webkit-backface-visibility: hidden;
}
/* 字体大小适配 */
.font-size-adaptive {
font-size: calc(16px + (20 - 16) * ((100vw - 320px) / (750 - 320)));
}
@media (min-width: 750px) {
.font-size-adaptive {
font-size: 20px;
}
}
@media (max-width: 320px) {
.font-size-adaptive {
font-size: 16px;
}
}
/* ==================== 布局兼容 ==================== */
/* Flex布局兼容 */
.flex {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
}
.flex-center {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-box-align: center;
-webkit-align-items: center;
-ms-flex-align: center;
align-items: center;
-webkit-box-pack: center;
-webkit-justify-content: center;
-ms-flex-pack: center;
justify-content: center;
}
.flex-between {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-box-pack: justify;
-webkit-justify-content: space-between;
-ms-flex-pack: justify;
justify-content: space-between;
}
.flex-column {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-box-orient: vertical;
-webkit-flex-direction: column;
-ms-flex-direction: column;
flex-direction: column;
}
/* Grid布局兼容 */
.grid {
display: -ms-grid;
display: grid;
}
/* ==================== 滚动优化 ==================== */
/* 修复移动端滚动 */
.scroll-fix {
-webkit-overflow-scrolling: touch;
overflow-y: auto;
overflow-x: hidden;
}
/* 隐藏滚动条但保留功能 */
.scrollbar-hide::-webkit-scrollbar {
display: none;
}
.scrollbar-hide {
-ms-overflow-style: none;
scrollbar-width: none;
}
/* ==================== 点击优化 ==================== */
/* 修复移动端点击延迟 */
.fast-click {
touch-action: manipulation;
-webkit-tap-highlight-color: transparent;
}
/* 点击反馈 */
.clickable {
cursor: pointer;
transition: opacity 0.2s ease;
}
.clickable:active {
opacity: 0.7;
}
/* ==================== 图片优化 ==================== */
/* 图片自适应 */
.img-responsive {
max-width: 100%;
height: auto;
display: block;
}
/* 图片懒加载占位 */
.img-placeholder {
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: placeholder-loading 1.5s infinite;
}
@keyframes placeholder-loading {
0% {
background-position: -200% 0;
}
100% {
background-position: 200% 0;
}
}
/* ==================== 文本优化 ==================== */
/* 文本省略 */
.text-ellipsis {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.text-ellipsis-2 {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
text-overflow: ellipsis;
}
.text-ellipsis-3 {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 3;
overflow: hidden;
text-overflow: ellipsis;
}
/* ==================== 安全区域 ==================== */
/* 刘海屏适配 */
.safe-area-top {
padding-top: constant(safe-area-inset-top);
padding-top: env(safe-area-inset-top);
}
.safe-area-bottom {
padding-bottom: constant(safe-area-inset-bottom);
padding-bottom: env(safe-area-inset-bottom);
}
.safe-area-left {
padding-left: constant(safe-area-inset-left);
padding-left: env(safe-area-inset-left);
}
.safe-area-right {
padding-right: constant(safe-area-inset-right);
padding-right: env(safe-area-inset-right);
}
.safe-area-all {
padding: constant(safe-area-inset-top) constant(safe-area-inset-right) constant(safe-area-inset-bottom) constant(safe-area-inset-left);
padding: env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left);
}
/* 横屏适配 */
@media screen and (orientation: landscape) {
.safe-area-bottom {
padding-bottom: constant(safe-area-inset-left);
padding-bottom: env(safe-area-inset-left);
}
.safe-area-top {
padding-top: constant(safe-area-inset-right);
padding-top: env(safe-area-inset-right);
}
}
/* ==================== 平台特定样式 ==================== */
/* H5平台特定样式 */
/* #ifdef H5 */
.h5-only {
display: block;
}
@media (hover: hover) {
.hover-effect:hover {
opacity: 0.8;
cursor: pointer;
}
}
/* #endif */
/* App平台特定样式 */
/* #ifdef APP-PLUS */
.app-only {
display: block;
}
/* 状态栏高度适配 */
.status-bar-height {
height: var(--status-bar-height, 20px);
}
/* #endif */
/* 微信小程序特定样式 */
/* #ifdef MP-WEIXIN */
.mp-weixin-only {
display: block;
}
/* #endif */
/* ==================== 响应式断点优化 ==================== */
/* 超小屏设备 */
@media screen and (max-width: 320px) {
.container {
padding: 10px;
}
.grid-item {
width: 50%;
}
}
/* 小屏设备 */
@media screen and (min-width: 321px) and (max-width: 375px) {
.container {
padding: 12px;
}
.grid-item {
width: 33.33%;
}
}
/* 中等屏设备 */
@media screen and (min-width: 376px) and (max-width: 414px) {
.container {
padding: 14px;
}
.grid-item {
width: 25%;
}
}
/* 大屏设备 */
@media screen and (min-width: 415px) and (max-width: 768px) {
.container {
padding: 16px;
}
.grid-item {
width: 20%;
}
}
/* 超大屏设备 */
@media screen and (min-width: 769px) {
.container {
padding: 20px;
max-width: 1200px;
margin: 0 auto;
}
.grid-item {
width: 16.66%;
}
}
/* ==================== 动画优化 ==================== */
/* 硬件加速 */
.hardware-accelerate {
-webkit-transform: translateZ(0);
transform: translateZ(0);
-webkit-backface-visibility: hidden;
backface-visibility: hidden;
-webkit-perspective: 1000;
perspective: 1000;
}
/* 平滑过渡 */
.smooth-transition {
-webkit-transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
/* ==================== 工具类 ==================== */
/* 清除浮动 */
.clearfix::after {
content: '';
display: table;
clear: both;
}
/* 居中 */
.center {
margin-left: auto;
margin-right: auto;
}
/* 隐藏元素 */
.hidden {
display: none !important;
}
/* 可见性 */
.invisible {
visibility: hidden;
}
/* 绝对定位 */
.absolute {
position: absolute;
}
/* 相对定位 */
.relative {
position: relative;
}
/* 固定定位 */
.fixed {
position: fixed;
}
/* 层级 */
.z-1 {
z-index: 1;
}
.z-10 {
z-index: 10;
}
.z-100 {
z-index: 100;
}
.z-1000 {
z-index: 1000;
}

90
static/scss/global.scss Normal file
View File

@ -0,0 +1,90 @@
.text-center {
text-align: center;
}
.font-13 {
font-size: 13px;
}
.font-12 {
font-size: 12px;
}
.font-11 {
font-size: 11px;
}
.text-grey1 {
color: #888;
}
.text-grey2 {
color: #aaa;
}
.list-cell-arrow::before {
content: ' ';
height: 10px;
width: 10px;
border-width: 2px 2px 0 0;
border-color: #c0c0c0;
border-style: solid;
-webkit-transform: matrix(0.5, 0.5, -0.5, 0.5, 0, 0);
transform: matrix(0.5, 0.5, -0.5, 0.5, 0, 0);
position: absolute;
top: 50%;
margin-top: -6px;
right: 30rpx;
}
.list-cell {
position: relative;
width: 100%;
box-sizing: border-box;
background-color: #fff;
color: #333;
padding: 26rpx 30rpx;
}
.list-cell:first-child {
border-radius: 8rpx 8rpx 0 0;
}
.list-cell:last-child {
border-radius: 0 0 8rpx 8rpx;
}
.list-cell::after {
content: '';
position: absolute;
border-bottom: 1px solid #eaeef1;
-webkit-transform: scaleY(0.5) translateZ(0);
transform: scaleY(0.5) translateZ(0);
transform-origin: 0 100%;
bottom: 0;
right: 0;
left: 0;
pointer-events: none;
}
.menu-list {
margin: 15px 15px;
.menu-item-box {
width: 100%;
display: flex;
align-items: center;
.menu-icon {
color: #007AFF;
font-size: 16px;
margin-right: 5px;
}
.text-right {
margin-left: auto;
margin-right: 34rpx;
color: #999;
}
}
}

6
static/scss/index.scss Normal file
View File

@ -0,0 +1,6 @@
// global
@import "./global.scss";
// color-ui
@import "@/static/scss/colorui.css";
// iconfont
@import "@/static/font/iconfont.css";

View File

@ -0,0 +1,398 @@
/* 全局样式配置文件 */
/* ==================== 主题色系 ==================== */
/* 浅色主题 */
:root {
--bg-primary: #f5f7fa;
--bg-secondary: #ffffff;
--bg-tertiary: #fafafa;
--text-primary: #333333;
--text-secondary: #888888;
--text-tertiary: #999999;
--border-color: #e0e0e0;
--border-light: #f0f0f0;
--shadow-color: rgba(0, 0, 0, 0.05);
--shadow-strong: rgba(0, 0, 0, 0.1);
--accent-color: #667eea;
--accent-hover: #5568d3;
--gradient-start: #f5f7fa;
--gradient-end: #c3cfe2;
--divider-color: #e0e0e0;
--card-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.05);
--card-border: 1rpx solid #e0e0e0;
--radius-sm: 8rpx;
--radius-md: 12rpx;
--radius-lg: 16rpx;
--radius-xl: 24rpx;
--spacing-xs: 8rpx;
--spacing-sm: 12rpx;
--spacing-md: 16rpx;
--spacing-lg: 20rpx;
--spacing-xl: 24rpx;
--spacing-xxl: 32rpx;
--font-xs: 20rpx;
--font-sm: 24rpx;
--font-md: 28rpx;
--font-lg: 32rpx;
--font-xl: 36rpx;
--font-xxl: 42rpx;
--line-height-sm: 1.4;
--line-height-md: 1.5;
--line-height-lg: 1.6;
}
/* 深色主题 */
.theme-dark {
--bg-primary: #1a1a1a;
--bg-secondary: #2d2d2d;
--bg-tertiary: #3a3a3a;
--text-primary: #ffffff;
--text-secondary: #aaaaaa;
--text-tertiary: #888888;
--border-color: #404040;
--border-light: #3a3a3a;
--shadow-color: rgba(0, 0, 0, 0.3);
--shadow-strong: rgba(0, 0, 0, 0.5);
--accent-color: #667eea;
--accent-hover: #7a8fd8;
--gradient-start: #1a1a1a;
--gradient-end: #2d2d2d;
--divider-color: #404040;
--card-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.3);
--card-border: 1rpx solid #404040;
}
/* ==================== 响应式断点 ==================== */
/* 超小屏手机 */
@media screen and (max-width: 375rpx) {
:root {
--font-xs: 18rpx;
--font-sm: 22rpx;
--font-md: 26rpx;
--font-lg: 30rpx;
--spacing-xs: 6rpx;
--spacing-sm: 10rpx;
--spacing-md: 14rpx;
--spacing-lg: 18rpx;
}
}
/* 小屏手机 */
@media screen and (min-width: 376rpx) and (max-width: 480rpx) {
:root {
--font-xs: 20rpx;
--font-sm: 24rpx;
--font-md: 28rpx;
--font-lg: 32rpx;
--spacing-xs: 8rpx;
--spacing-sm: 12rpx;
--spacing-md: 16rpx;
--spacing-lg: 20rpx;
}
}
/* 中等屏幕手机 */
@media screen and (min-width: 481rpx) and (max-width: 600rpx) {
:root {
--font-xs: 22rpx;
--font-sm: 26rpx;
--font-md: 30rpx;
--font-lg: 34rpx;
--spacing-xs: 10rpx;
--spacing-sm: 14rpx;
--spacing-md: 18rpx;
--spacing-lg: 22rpx;
}
}
/* 大屏手机/平板 */
@media screen and (min-width: 601rpx) and (max-width: 750rpx) {
:root {
--font-xs: 24rpx;
--font-sm: 28rpx;
--font-md: 32rpx;
--font-lg: 36rpx;
--spacing-xs: 12rpx;
--spacing-sm: 16rpx;
--spacing-md: 20rpx;
--spacing-lg: 24rpx;
}
}
/* 超大屏/桌面 */
@media screen and (min-width: 751rpx) {
:root {
--font-xs: 26rpx;
--font-sm: 30rpx;
--font-md: 34rpx;
--font-lg: 38rpx;
--spacing-xs: 14rpx;
--spacing-sm: 18rpx;
--spacing-md: 22rpx;
--spacing-lg: 26rpx;
}
}
/* ==================== 通用工具类 ==================== */
/* 布局 */
.flex {
display: flex;
}
.flex-center {
display: flex;
align-items: center;
}
.flex-between {
display: flex;
justify-content: space-between;
}
.flex-column {
display: flex;
flex-direction: column;
}
.flex-wrap {
flex-wrap: wrap;
}
.grid {
display: grid;
}
/* 间距 */
.gap-xs {
gap: var(--spacing-xs);
}
.gap-sm {
gap: var(--spacing-sm);
}
.gap-md {
gap: var(--spacing-md);
}
.gap-lg {
gap: var(--spacing-lg);
}
.padding-xs {
padding: var(--spacing-xs);
}
.padding-sm {
padding: var(--spacing-sm);
}
.padding-md {
padding: var(--spacing-md);
}
.padding-lg {
padding: var(--spacing-lg);
}
.padding-xl {
padding: var(--spacing-xl);
}
.margin-xs {
margin: var(--spacing-xs);
}
.margin-sm {
margin: var(--spacing-sm);
}
.margin-md {
margin: var(--spacing-md);
}
.margin-lg {
margin: var(--spacing-lg);
}
.margin-xl {
margin: var(--spacing-xl);
}
/* 文本 */
.text-xs {
font-size: var(--font-xs);
}
.text-sm {
font-size: var(--font-sm);
}
.text-md {
font-size: var(--font-md);
}
.text-lg {
font-size: var(--font-lg);
}
.text-xl {
font-size: var(--font-xl);
}
.text-center {
text-align: center;
}
.text-left {
text-align: left;
}
.text-right {
text-align: right;
}
.text-primary {
color: var(--text-primary);
}
.text-secondary {
color: var(--text-secondary);
}
.text-tertiary {
color: var(--text-tertiary);
}
/* 卡片 */
.card {
background: var(--bg-secondary);
border-radius: var(--radius-md);
box-shadow: var(--card-shadow);
border: var(--card-border);
}
.card-hover {
box-shadow: var(--shadow-strong);
transform: translateY(-2rpx);
transition: all 0.3s ease;
}
/* 按钮 */
.btn {
background: var(--accent-color);
color: #ffffff;
border-radius: var(--radius-md);
padding: var(--spacing-sm) var(--spacing-lg);
font-size: var(--font-md);
border: none;
cursor: pointer;
transition: all 0.3s ease;
}
.btn-hover {
background: var(--accent-hover);
transform: translateY(-2rpx);
}
.btn-outline {
background: transparent;
color: var(--accent-color);
border: 2rpx solid var(--accent-color);
}
.btn-sm {
padding: var(--spacing-xs) var(--spacing-sm);
font-size: var(--font-sm);
}
.btn-lg {
padding: var(--spacing-md) var(--spacing-xl);
font-size: var(--font-lg);
}
/* 输入框 */
.input {
background: var(--bg-secondary);
border: var(--card-border);
border-radius: var(--radius-sm);
padding: var(--spacing-sm) var(--spacing-md);
font-size: var(--font-md);
color: var(--text-primary);
}
.input-focus {
border-color: var(--accent-color);
box-shadow: 0 0 4rpx var(--shadow-color);
}
/* 分割线 */
.divider {
height: 1rpx;
background: var(--divider-color);
margin: var(--spacing-md) 0;
}
.divider-vertical {
width: 1rpx;
background: var(--divider-color);
margin: 0 var(--spacing-md);
}
/* 徽章 */
.badge {
background: var(--accent-color);
color: #ffffff;
border-radius: var(--radius-sm);
padding: var(--spacing-xs) var(--spacing-sm);
font-size: var(--font-xs);
}
/* 骨架 */
.skeleton {
background: linear-gradient(90deg, var(--bg-secondary) 25%, var(--bg-tertiary) 50%, var(--bg-secondary) 75%);
border-radius: var(--radius-sm);
animation: skeleton-loading 1.5s infinite;
}
@keyframes skeleton-loading {
0% {
background-position: -200% 0;
}
100% {
background-position: 200% 0;
}
}
/* 隐藏滚动条 */
::-webkit-scrollbar {
width: 6rpx;
height: 6rpx;
}
::-webkit-scrollbar-thumb {
background: var(--border-color);
border-radius: 3rpx;
}
/* 安全区域适配 */
.safe-area-bottom {
padding-bottom: constant(safe-area-inset-bottom);
padding-bottom: env(safe-area-inset-bottom);
}
.safe-area-top {
padding-top: constant(safe-area-inset-top);
padding-top: env(safe-area-inset-top);
}
/* 横屏适配 */
@media screen and (orientation: landscape) {
.safe-area-bottom {
padding-bottom: constant(safe-area-inset-left);
padding-bottom: env(safe-area-inset-left);
}
}

9
store/getters.js Normal file
View File

@ -0,0 +1,9 @@
const getters = {
token: state => state.user.token,
avatar: state => state.user.avatar,
id: state => state.user.id,
name: state => state.user.name,
roles: state => state.user.roles,
permissions: state => state.user.permissions
}
export default getters

15
store/index.js Normal file
View File

@ -0,0 +1,15 @@
import Vue from 'vue'
import Vuex from 'vuex'
import user from '@/store/modules/user'
import getters from './getters'
Vue.use(Vuex)
const store = new Vuex.Store({
modules: {
user
},
getters
})
export default store

111
store/modules/user.js Normal file
View File

@ -0,0 +1,111 @@
import config from './../../config'
import storage from '@/utils/storage'
import constant from '@/utils/constant'
import { isHttp, isEmpty } from "@/utils/validate"
import { login, logout, getInfo } from '@/api/login'
import { getToken, setToken, removeToken } from '@/utils/auth'
import defAva from '@/static/images/profile.jpg'
const baseUrl = config.baseUrl
console.log('User module base URL:', baseUrl)
const user = {
state: {
token: getToken(),
id: storage.get(constant.id),
name: storage.get(constant.name),
avatar: storage.get(constant.avatar),
roles: storage.get(constant.roles),
permissions: storage.get(constant.permissions)
},
mutations: {
SET_TOKEN: (state, token) => {
state.token = token
},
SET_ID: (state, id) => {
state.id = id
storage.set(constant.id, id)
},
SET_NAME: (state, name) => {
state.name = name
storage.set(constant.name, name)
},
SET_AVATAR: (state, avatar) => {
state.avatar = avatar
storage.set(constant.avatar, avatar)
},
SET_ROLES: (state, roles) => {
state.roles = roles
storage.set(constant.roles, roles)
},
SET_PERMISSIONS: (state, permissions) => {
state.permissions = permissions
storage.set(constant.permissions, permissions)
}
},
actions: {
// 登录
Login({ commit }, userInfo) {
const username = userInfo.username.trim()
const password = userInfo.password
const code = userInfo.code
const uuid = userInfo.uuid
return new Promise((resolve, reject) => {
login(username, password, code, uuid).then(res => {
setToken(res.token)
commit('SET_TOKEN', res.token)
resolve()
}).catch(error => {
reject(error)
})
})
},
// 获取用户信息
GetInfo({ commit, state }) {
return new Promise((resolve, reject) => {
getInfo().then(res => {
const user = res.user
let avatar = (isEmpty(user) || isEmpty(user.avatar)) ? "" : user.avatar
if (!isHttp(avatar)) {
avatar = (isEmpty(avatar)) ? defAva : baseUrl + avatar
}
const userid = (isEmpty(user) || isEmpty(user.userId)) ? "" : user.userId
const username = (isEmpty(user) || isEmpty(user.userName)) ? "" : user.userName
if (res.roles && res.roles.length > 0) {
commit('SET_ROLES', res.roles)
commit('SET_PERMISSIONS', res.permissions)
} else {
commit('SET_ROLES', ['ROLE_DEFAULT'])
}
commit('SET_ID', userid)
commit('SET_NAME', username)
commit('SET_AVATAR', avatar)
resolve(res)
}).catch(error => {
reject(error)
})
})
},
// 退出系统
LogOut({ commit, state }) {
return new Promise((resolve, reject) => {
logout(state.token).then(() => {
commit('SET_TOKEN', '')
commit('SET_ROLES', [])
commit('SET_PERMISSIONS', [])
removeToken()
storage.clean()
resolve()
}).catch(error => {
reject(error)
})
})
}
}
}
export default user

64
uni.scss Normal file
View File

@ -0,0 +1,64 @@
/**
* uni-app
*/
/* 行为相关颜色 */
$uni-color-primary: #007aff;
$uni-color-success: #4cd964;
$uni-color-warning: #f0ad4e;
$uni-color-error: #dd524d;
/* 文字基本颜色 */
$uni-text-color:#333;//
$uni-text-color-inverse:#fff;//
$uni-text-color-grey:#999;//
$uni-text-color-placeholder: #808080;
$uni-text-color-disable:#c0c0c0;
/* 背景颜色 */
$uni-bg-color:#ffffff;
$uni-bg-color-grey:#f8f8f8;
$uni-bg-color-hover:#f1f1f1;//
$uni-bg-color-mask:rgba(0, 0, 0, 0.4);//
/* 边框颜色 */
$uni-border-color:#e5e5e5;
/* 尺寸变量 */
/* 文字尺寸 */
$uni-font-size-sm:12px;
$uni-font-size-base:14px;
$uni-font-size-lg:16px;
/* 图片尺寸 */
$uni-img-size-sm:20px;
$uni-img-size-base:26px;
$uni-img-size-lg:40px;
/* Border Radius */
$uni-border-radius-sm: 2px;
$uni-border-radius-base: 3px;
$uni-border-radius-lg: 6px;
$uni-border-radius-circle: 50%;
/* 水平间距 */
$uni-spacing-row-sm: 5px;
$uni-spacing-row-base: 10px;
$uni-spacing-row-lg: 15px;
/* 垂直间距 */
$uni-spacing-col-sm: 4px;
$uni-spacing-col-base: 8px;
$uni-spacing-col-lg: 12px;
/* 透明度 */
$uni-opacity-disabled: 0.3; //
/* 文章场景相关 */
$uni-color-title: #2C405A; //
$uni-font-size-title:20px;
$uni-color-subtitle: #555555; //
$uni-font-size-subtitle:26px;
$uni-color-paragraph: #3F536E; //
$uni-font-size-paragraph:15px;

View File

@ -0,0 +1,33 @@
## 1.2.22023-01-28
- 修复 运行/打包 控制台警告问题
## 1.2.12022-09-05
- 修复 当 text 超过 max-num 时badge 的宽度计算是根据 text 的长度计算,更改为 css 计算实际展示宽度,详见:[https://ask.dcloud.net.cn/question/150473](https://ask.dcloud.net.cn/question/150473)
## 1.2.02021-11-19
- 优化 组件UI并提供设计资源详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource)
- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-badge](https://uniapp.dcloud.io/component/uniui/uni-badge)
## 1.1.72021-11-08
- 优化 升级ui
- 修改 size 属性默认值调整为 small
- 修改 type 属性,默认值调整为 errorinfo 替换 default
## 1.1.62021-09-22
- 修复 在字节小程序上样式不生效的 bug
## 1.1.52021-07-30
- 组件兼容 vue3如何创建vue3项目详见 [uni-app 项目支持 vue3 介绍](https://ask.dcloud.net.cn/article/37834)
## 1.1.42021-07-29
- 修复 去掉 nvue 不支持css 的 align-self 属性nvue 下不暂支持 absolute 属性
## 1.1.32021-06-24
- 优化 示例项目
## 1.1.12021-05-12
- 新增 组件示例地址
## 1.1.02021-05-12
- 新增 uni-badge 的 absolute 属性,支持定位
- 新增 uni-badge 的 offset 属性,支持定位偏移
- 新增 uni-badge 的 is-dot 属性,支持仅显示有一个小点
- 新增 uni-badge 的 max-num 属性,支持自定义封顶的数字值,超过 99 显示99+
- 优化 uni-badge 属性 custom-style 支持以对象形式自定义样式
## 1.0.72021-05-07
- 修复 uni-badge 在 App 端数字小于10时不是圆形的bug
- 修复 uni-badge 在父元素不是 flex 布局时宽度缩小的bug
- 新增 uni-badge 属性 custom-style 支持自定义样式
## 1.0.62021-02-04
- 调整为uni_modules目录规范

View File

@ -0,0 +1,268 @@
<template>
<view class="uni-badge--x">
<slot />
<text v-if="text" :class="classNames" :style="[positionStyle, customStyle, dotStyle]"
class="uni-badge" @click="onClick()">{{displayValue}}</text>
</view>
</template>
<script>
/**
* Badge 数字角标
* @description 数字角标一般和其它控件列表9宫格等配合使用用于进行数量提示默认为实心灰色背景
* @tutorial https://ext.dcloud.net.cn/plugin?id=21
* @property {String} text 角标内容
* @property {String} size = [normal|small] 角标内容
* @property {String} type = [info|primary|success|warning|error] 颜色类型
* @value info 灰色
* @value primary 蓝色
* @value success 绿色
* @value warning 黄色
* @value error 红色
* @property {String} inverted = [true|false] 是否无需背景颜色
* @property {Number} maxNum 展示封顶的数字值超过 99 显示 99+
* @property {String} absolute = [rightTop|rightBottom|leftBottom|leftTop] 开启绝对定位, 角标将定位到其包裹的标签的四角上
* @value rightTop 右上
* @value rightBottom 右下
* @value leftTop 左上
* @value leftBottom 左下
* @property {Array[number]} offset 距定位角中心点的偏移量只有存在 absolute 属性时有效例如[-10, -10] 表示向外偏移 10px[10, 10] 表示向 absolute 指定的内偏移 10px
* @property {String} isDot = [true|false] 是否显示为一个小点
* @event {Function} click 点击 Badge 触发事件
* @example <uni-badge text="1"></uni-badge>
*/
export default {
name: 'UniBadge',
emits: ['click'],
props: {
type: {
type: String,
default: 'error'
},
inverted: {
type: Boolean,
default: false
},
isDot: {
type: Boolean,
default: false
},
maxNum: {
type: Number,
default: 99
},
absolute: {
type: String,
default: ''
},
offset: {
type: Array,
default () {
return [0, 0]
}
},
text: {
type: [String, Number],
default: ''
},
size: {
type: String,
default: 'small'
},
customStyle: {
type: Object,
default () {
return {}
}
}
},
data() {
return {};
},
computed: {
width() {
return String(this.text).length * 8 + 12
},
classNames() {
const {
inverted,
type,
size,
absolute
} = this
return [
inverted ? 'uni-badge--' + type + '-inverted' : '',
'uni-badge--' + type,
'uni-badge--' + size,
absolute ? 'uni-badge--absolute' : ''
].join(' ')
},
positionStyle() {
if (!this.absolute) return {}
let w = this.width / 2,
h = 10
if (this.isDot) {
w = 5
h = 5
}
const x = `${- w + this.offset[0]}px`
const y = `${- h + this.offset[1]}px`
const whiteList = {
rightTop: {
right: x,
top: y
},
rightBottom: {
right: x,
bottom: y
},
leftBottom: {
left: x,
bottom: y
},
leftTop: {
left: x,
top: y
}
}
const match = whiteList[this.absolute]
return match ? match : whiteList['rightTop']
},
dotStyle() {
if (!this.isDot) return {}
return {
width: '10px',
minWidth: '0',
height: '10px',
padding: '0',
borderRadius: '10px'
}
},
displayValue() {
const {
isDot,
text,
maxNum
} = this
return isDot ? '' : (Number(text) > maxNum ? `${maxNum}+` : text)
}
},
methods: {
onClick() {
this.$emit('click');
}
}
};
</script>
<style lang="scss" >
$uni-primary: #2979ff !default;
$uni-success: #4cd964 !default;
$uni-warning: #f0ad4e !default;
$uni-error: #dd524d !default;
$uni-info: #909399 !default;
$bage-size: 12px;
$bage-small: scale(0.8);
.uni-badge--x {
/* #ifdef APP-NVUE */
// align-self: flex-start;
/* #endif */
/* #ifndef APP-NVUE */
display: inline-block;
/* #endif */
position: relative;
}
.uni-badge--absolute {
position: absolute;
}
.uni-badge--small {
transform: $bage-small;
transform-origin: center center;
}
.uni-badge {
/* #ifndef APP-NVUE */
display: flex;
overflow: hidden;
box-sizing: border-box;
font-feature-settings: "tnum";
min-width: 20px;
/* #endif */
justify-content: center;
flex-direction: row;
height: 20px;
padding: 0 4px;
line-height: 18px;
color: #fff;
border-radius: 100px;
background-color: $uni-info;
background-color: transparent;
border: 1px solid #fff;
text-align: center;
font-family: 'Helvetica Neue', Helvetica, sans-serif;
font-size: $bage-size;
/* #ifdef H5 */
z-index: 999;
cursor: pointer;
/* #endif */
&--info {
color: #fff;
background-color: $uni-info;
}
&--primary {
background-color: $uni-primary;
}
&--success {
background-color: $uni-success;
}
&--warning {
background-color: $uni-warning;
}
&--error {
background-color: $uni-error;
}
&--inverted {
padding: 0 5px 0 0;
color: $uni-info;
}
&--info-inverted {
color: $uni-info;
background-color: transparent;
}
&--primary-inverted {
color: $uni-primary;
background-color: transparent;
}
&--success-inverted {
color: $uni-success;
background-color: transparent;
}
&--warning-inverted {
color: $uni-warning;
background-color: transparent;
}
&--error-inverted {
color: $uni-error;
background-color: transparent;
}
}
</style>

View File

@ -0,0 +1,85 @@
{
"id": "uni-badge",
"displayName": "uni-badge 数字角标",
"version": "1.2.2",
"description": "数字角标(徽章)组件,在元素周围展示消息提醒,一般用于列表、九宫格、按钮等地方。",
"keywords": [
"",
"badge",
"uni-ui",
"uniui",
"数字角标",
"徽章"
],
"repository": "https://github.com/dcloudio/uni-ui",
"engines": {
"HBuilderX": ""
},
"directories": {
"example": "../../temps/example_temps"
},
"dcloudext": {
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": ""
},
"declaration": {
"ads": "无",
"data": "无",
"permissions": "无"
},
"npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui",
"type": "component-vue"
},
"uni_modules": {
"dependencies": ["uni-scss"],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "y",
"aliyun": "y"
},
"client": {
"App": {
"app-vue": "y",
"app-nvue": "y"
},
"H5-mobile": {
"Safari": "y",
"Android Browser": "y",
"微信浏览器(Android)": "y",
"QQ浏览器(Android)": "y"
},
"H5-pc": {
"Chrome": "y",
"IE": "y",
"Edge": "y",
"Firefox": "y",
"Safari": "y"
},
"小程序": {
"微信": "y",
"阿里": "y",
"百度": "y",
"字节跳动": "y",
"QQ": "y"
},
"快应用": {
"华为": "y",
"联盟": "y"
},
"Vue": {
"vue2": "y",
"vue3": "y"
}
}
}
}
}

View File

@ -0,0 +1,10 @@
## Badge 数字角标
> **组件名uni-badge**
> 代码块: `uBadge`
数字角标一般和其它控件列表、9宫格等配合使用用于进行数量提示默认为实心灰色背景
### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-badge)
#### 如使用过程中有任何问题或者您对uni-ui有一些好的建议欢迎加入 uni-ui 交流群871950839

View File

@ -0,0 +1,26 @@
## 1.3.12021-12-20
- 修复 在vue页面下略缩图显示不正常的bug
## 1.3.02021-11-19
- 重构插槽的用法 header 替换为 title
- 新增 actions 插槽
- 新增 cover 封面图属性和插槽
- 新增 padding 内容默认内边距离
- 新增 margin 卡片默认外边距离
- 新增 spacing 卡片默认内边距
- 新增 shadow 卡片阴影属性
- 取消 mode 属性,可使用组合插槽代替
- 取消 note 属性 使用actions插槽代替
- 优化 组件UI并提供设计资源详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource)
- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-card](https://uniapp.dcloud.io/component/uniui/uni-card)
## 1.2.12021-07-30
- 优化 vue3下事件警告的问题
## 1.2.02021-07-13
- 组件兼容 vue3如何创建vue3项目详见 [uni-app 项目支持 vue3 介绍](https://ask.dcloud.net.cn/article/37834)
## 1.1.82021-07-01
- 优化 图文卡片无图片加载时,提供占位图标
- 新增 header 插槽,自定义卡片头部( 图文卡片 mode="style" 时,不支持)
- 修复 thumbnail 不存在仍然占位的 bug
## 1.1.72021-05-12
- 新增 组件示例地址
## 1.1.62021-02-04
- 调整为uni_modules目录规范

View File

@ -0,0 +1,270 @@
<template>
<view class="uni-card" :class="{ 'uni-card--full': isFull, 'uni-card--shadow': isShadow,'uni-card--border':border}"
:style="{'margin':isFull?0:margin,'padding':spacing,'box-shadow':isShadow?shadow:''}">
<!-- 封面 -->
<slot name="cover">
<view v-if="cover" class="uni-card__cover">
<image class="uni-card__cover-image" mode="widthFix" @click="onClick('cover')" :src="cover"></image>
</view>
</slot>
<slot name="title">
<view v-if="title || extra" class="uni-card__header">
<!-- 卡片标题 -->
<view class="uni-card__header-box" @click="onClick('title')">
<view v-if="thumbnail" class="uni-card__header-avatar">
<image class="uni-card__header-avatar-image" :src="thumbnail" mode="aspectFit" />
</view>
<view class="uni-card__header-content">
<text class="uni-card__header-content-title uni-ellipsis">{{ title }}</text>
<text v-if="title&&subTitle"
class="uni-card__header-content-subtitle uni-ellipsis">{{ subTitle }}</text>
</view>
</view>
<view class="uni-card__header-extra" @click="onClick('extra')">
<text class="uni-card__header-extra-text">{{ extra }}</text>
</view>
</view>
</slot>
<!-- 卡片内容 -->
<view class="uni-card__content" :style="{padding:padding}" @click="onClick('content')">
<slot></slot>
</view>
<view class="uni-card__actions" @click="onClick('actions')">
<slot name="actions"></slot>
</view>
</view>
</template>
<script>
/**
* Card 卡片
* @description 卡片视图组件
* @tutorial https://ext.dcloud.net.cn/plugin?id=22
* @property {String} title 标题文字
* @property {String} subTitle 副标题
* @property {Number} padding 内容内边距
* @property {Number} margin 卡片外边距
* @property {Number} spacing 卡片内边距
* @property {String} extra 标题额外信息
* @property {String} cover 封面图本地路径需要引入
* @property {String} thumbnail 标题左侧缩略图
* @property {Boolean} is-full = [true | false] 卡片内容是否通栏 true 时将去除padding值
* @property {Boolean} is-shadow = [true | false] 卡片内容是否开启阴影
* @property {String} shadow 卡片阴影
* @property {Boolean} border 卡片边框
* @event {Function} click 点击 Card 触发事件
*/
export default {
name: 'UniCard',
emits: ['click'],
props: {
title: {
type: String,
default: ''
},
subTitle: {
type: String,
default: ''
},
padding: {
type: String,
default: '10px'
},
margin: {
type: String,
default: '15px'
},
spacing: {
type: String,
default: '0 10px'
},
extra: {
type: String,
default: ''
},
cover: {
type: String,
default: ''
},
thumbnail: {
type: String,
default: ''
},
isFull: {
//
type: Boolean,
default: false
},
isShadow: {
//
type: Boolean,
default: true
},
shadow: {
type: String,
default: '0px 0px 3px 1px rgba(0, 0, 0, 0.08)'
},
border: {
type: Boolean,
default: true
}
},
methods: {
onClick(type) {
this.$emit('click', type)
}
}
}
</script>
<style lang="scss">
$uni-border-3: #EBEEF5 !default;
$uni-shadow-base:0 0px 6px 1px rgba($color: #a5a5a5, $alpha: 0.2) !default;
$uni-main-color: #3a3a3a !default;
$uni-base-color: #6a6a6a !default;
$uni-secondary-color: #909399 !default;
$uni-spacing-sm: 8px !default;
$uni-border-color:$uni-border-3;
$uni-shadow: $uni-shadow-base;
$uni-card-title: 15px;
$uni-cart-title-color:$uni-main-color;
$uni-card-subtitle: 12px;
$uni-cart-subtitle-color:$uni-secondary-color;
$uni-card-spacing: 10px;
$uni-card-content-color: $uni-base-color;
.uni-card {
margin: $uni-card-spacing;
padding: 0 $uni-spacing-sm;
border-radius: 4px;
overflow: hidden;
font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, SimSun, sans-serif;
background-color: #fff;
flex: 1;
.uni-card__cover {
position: relative;
margin-top: $uni-card-spacing;
flex-direction: row;
overflow: hidden;
border-radius: 4px;
.uni-card__cover-image {
flex: 1;
// width: 100%;
/* #ifndef APP-PLUS */
vertical-align: middle;
/* #endif */
}
}
.uni-card__header {
display: flex;
border-bottom: 1px $uni-border-color solid;
flex-direction: row;
align-items: center;
padding: $uni-card-spacing;
overflow: hidden;
.uni-card__header-box {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex: 1;
flex-direction: row;
align-items: center;
overflow: hidden;
}
.uni-card__header-avatar {
width: 40px;
height: 40px;
overflow: hidden;
border-radius: 5px;
margin-right: $uni-card-spacing;
.uni-card__header-avatar-image {
flex: 1;
width: 40px;
height: 40px;
}
}
.uni-card__header-content {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: column;
justify-content: center;
flex: 1;
// height: 40px;
overflow: hidden;
.uni-card__header-content-title {
font-size: $uni-card-title;
color: $uni-cart-title-color;
// line-height: 22px;
}
.uni-card__header-content-subtitle {
font-size: $uni-card-subtitle;
margin-top: 5px;
color: $uni-cart-subtitle-color;
}
}
.uni-card__header-extra {
line-height: 12px;
.uni-card__header-extra-text {
font-size: 12px;
color: $uni-cart-subtitle-color;
}
}
}
.uni-card__content {
padding: $uni-card-spacing;
font-size: 14px;
color: $uni-card-content-color;
line-height: 22px;
}
.uni-card__actions {
font-size: 12px;
}
}
.uni-card--border {
border: 1px solid $uni-border-color;
}
.uni-card--shadow {
position: relative;
/* #ifndef APP-NVUE */
box-shadow: $uni-shadow;
/* #endif */
}
.uni-card--full {
margin: 0;
border-left-width: 0;
border-left-width: 0;
border-radius: 0;
}
/* #ifndef APP-NVUE */
.uni-card--full:after {
border-radius: 0;
}
/* #endif */
.uni-ellipsis {
/* #ifndef APP-NVUE */
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
/* #endif */
/* #ifdef APP-NVUE */
lines: 1;
/* #endif */
}
</style>

View File

@ -0,0 +1,90 @@
{
"id": "uni-card",
"displayName": "uni-card 卡片",
"version": "1.3.1",
"description": "Card 组件,提供常见的卡片样式。",
"keywords": [
"uni-ui",
"uniui",
"card",
"",
"卡片"
],
"repository": "https://github.com/dcloudio/uni-ui",
"engines": {
"HBuilderX": ""
},
"directories": {
"example": "../../temps/example_temps"
},
"dcloudext": {
"category": [
"前端组件",
"通用组件"
],
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": ""
},
"declaration": {
"ads": "无",
"data": "无",
"permissions": "无"
},
"npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui"
},
"uni_modules": {
"dependencies": [
"uni-icons",
"uni-scss"
],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "y",
"aliyun": "y"
},
"client": {
"App": {
"app-vue": "y",
"app-nvue": "y"
},
"H5-mobile": {
"Safari": "y",
"Android Browser": "y",
"微信浏览器(Android)": "y",
"QQ浏览器(Android)": "y"
},
"H5-pc": {
"Chrome": "y",
"IE": "y",
"Edge": "y",
"Firefox": "y",
"Safari": "y"
},
"小程序": {
"微信": "y",
"阿里": "y",
"百度": "y",
"字节跳动": "y",
"QQ": "y"
},
"快应用": {
"华为": "u",
"联盟": "u"
},
"Vue": {
"vue2": "y",
"vue3": "y"
}
}
}
}
}

View File

@ -0,0 +1,12 @@
## Card 卡片
> **组件名uni-card**
> 代码块: `uCard`
卡片视图组件。
### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-card)
#### 如使用过程中有任何问题或者您对uni-ui有一些好的建议欢迎加入 uni-ui 交流群871950839

View File

@ -0,0 +1,51 @@
## 1.0.62024-10-22
- 新增 当 multiple 为 false 且传递的 value 为 数组时,使用数组第一项用作反显
## 1.0.52024-03-20
- 修复 单选模式下选中样式不生效的bug
## 1.0.42024-01-27
- 修复 修复错别字chagne为change
## 1.0.32022-09-16
- 可以使用 uni-scss 控制主题色
## 1.0.22022-06-30
- 优化 在 uni-forms 中的依赖注入方式
## 1.0.12022-02-07
- 修复 multiple 为 true 时v-model 的值为 null 报错的 bug
## 1.0.02021-11-19
- 优化 组件UI并提供设计资源详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource)
- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-data-checkbox](https://uniapp.dcloud.io/component/uniui/uni-data-checkbox)
## 0.2.52021-08-23
- 修复 在uni-forms中 modelValue 中不存在当前字段,当前字段必填写也不参与校验的问题
## 0.2.42021-08-17
- 修复 单选 list 模式下 icon 为 left 时,选中图标不显示的问题
## 0.2.32021-08-11
- 修复 在 uni-forms 中重置表单,错误信息无法清除的问题
## 0.2.22021-07-30
- 优化 在uni-forms组件与label不对齐的问题
## 0.2.12021-07-27
- 修复 单选默认值为0不能选中的Bug
## 0.2.02021-07-13
- 组件兼容 vue3如何创建vue3项目详见 [uni-app 项目支持 vue3 介绍](https://ask.dcloud.net.cn/article/37834)
## 0.1.112021-07-06
- 优化 删除无用日志
## 0.1.102021-07-05
- 修复 由 0.1.9 引起的非 nvue 端图标不显示的问题
## 0.1.92021-07-05
- 修复 nvue 黑框样式问题
## 0.1.82021-06-28
- 修复 selectedTextColor 属性不生效的Bug
## 0.1.72021-06-02
- 新增 map 属性可以方便映射text/value属性
## 0.1.62021-05-26
- 修复 不关联服务空间的情况下组件报错的Bug
## 0.1.52021-05-12
- 新增 组件示例地址
## 0.1.42021-04-09
- 修复 nvue 下无法选中的问题
## 0.1.32021-03-22
- 新增 disabled属性
## 0.1.22021-02-24
- 优化 默认颜色显示
## 0.1.12021-02-24
- 新增 支持nvue
## 0.1.02021-02-18
- “暂无数据”显示居中

View File

@ -0,0 +1,316 @@
const events = {
load: 'load',
error: 'error'
}
const pageMode = {
add: 'add',
replace: 'replace'
}
const attrs = [
'pageCurrent',
'pageSize',
'collection',
'action',
'field',
'getcount',
'orderby',
'where'
]
export default {
data() {
return {
loading: false,
listData: this.getone ? {} : [],
paginationInternal: {
current: this.pageCurrent,
size: this.pageSize,
count: 0
},
errorMessage: ''
}
},
created() {
let db = null;
let dbCmd = null;
if(this.collection){
this.db = uniCloud.database();
this.dbCmd = this.db.command;
}
this._isEnded = false
this.$watch(() => {
let al = []
attrs.forEach(key => {
al.push(this[key])
})
return al
}, (newValue, oldValue) => {
this.paginationInternal.pageSize = this.pageSize
let needReset = false
for (let i = 2; i < newValue.length; i++) {
if (newValue[i] != oldValue[i]) {
needReset = true
break
}
}
if (needReset) {
this.clear()
this.reset()
}
if (newValue[0] != oldValue[0]) {
this.paginationInternal.current = this.pageCurrent
}
this._execLoadData()
})
// #ifdef H5
if (process.env.NODE_ENV === 'development') {
this._debugDataList = []
if (!window.unidev) {
window.unidev = {
clientDB: {
data: []
}
}
}
unidev.clientDB.data.push(this._debugDataList)
}
// #endif
// #ifdef MP-TOUTIAO
let changeName
let events = this.$scope.dataset.eventOpts
for (let i = 0; i < events.length; i++) {
let event = events[i]
if (event[0].includes('^load')) {
changeName = event[1][0][0]
}
}
if (changeName) {
let parent = this.$parent
let maxDepth = 16
this._changeDataFunction = null
while (parent && maxDepth > 0) {
let fun = parent[changeName]
if (fun && typeof fun === 'function') {
this._changeDataFunction = fun
maxDepth = 0
break
}
parent = parent.$parent
maxDepth--;
}
}
// #endif
// if (!this.manual) {
// this.loadData()
// }
},
// #ifdef H5
beforeDestroy() {
if (process.env.NODE_ENV === 'development' && window.unidev) {
let cd = this._debugDataList
let dl = unidev.clientDB.data
for (let i = dl.length - 1; i >= 0; i--) {
if (dl[i] === cd) {
dl.splice(i, 1)
break
}
}
}
},
// #endif
methods: {
loadData(args1, args2) {
let callback = null
if (typeof args1 === 'object') {
if (args1.clear) {
this.clear()
this.reset()
}
if (args1.current !== undefined) {
this.paginationInternal.current = args1.current
}
if (typeof args2 === 'function') {
callback = args2
}
} else if (typeof args1 === 'function') {
callback = args1
}
this._execLoadData(callback)
},
loadMore() {
if (this._isEnded) {
return
}
this._execLoadData()
},
refresh() {
this.clear()
this._execLoadData()
},
clear() {
this._isEnded = false
this.listData = []
},
reset() {
this.paginationInternal.current = 1
},
remove(id, {
action,
callback,
confirmTitle,
confirmContent
} = {}) {
if (!id || !id.length) {
return
}
uni.showModal({
title: confirmTitle || '提示',
content: confirmContent || '是否删除该数据',
showCancel: true,
success: (res) => {
if (!res.confirm) {
return
}
this._execRemove(id, action, callback)
}
})
},
_execLoadData(callback) {
if (this.loading) {
return
}
this.loading = true
this.errorMessage = ''
this._getExec().then((res) => {
this.loading = false
const {
data,
count
} = res.result
this._isEnded = data.length < this.pageSize
callback && callback(data, this._isEnded)
this._dispatchEvent(events.load, data)
if (this.getone) {
this.listData = data.length ? data[0] : undefined
} else if (this.pageData === pageMode.add) {
this.listData.push(...data)
if (this.listData.length) {
this.paginationInternal.current++
}
} else if (this.pageData === pageMode.replace) {
this.listData = data
this.paginationInternal.count = count
}
// #ifdef H5
if (process.env.NODE_ENV === 'development') {
this._debugDataList.length = 0
this._debugDataList.push(...JSON.parse(JSON.stringify(this.listData)))
}
// #endif
}).catch((err) => {
this.loading = false
this.errorMessage = err
callback && callback()
this.$emit(events.error, err)
})
},
_getExec() {
let exec = this.db
if (this.action) {
exec = exec.action(this.action)
}
exec = exec.collection(this.collection)
if (!(!this.where || !Object.keys(this.where).length)) {
exec = exec.where(this.where)
}
if (this.field) {
exec = exec.field(this.field)
}
if (this.orderby) {
exec = exec.orderBy(this.orderby)
}
const {
current,
size
} = this.paginationInternal
exec = exec.skip(size * (current - 1)).limit(size).get({
getCount: this.getcount
})
return exec
},
_execRemove(id, action, callback) {
if (!this.collection || !id) {
return
}
const ids = Array.isArray(id) ? id : [id]
if (!ids.length) {
return
}
uni.showLoading({
mask: true
})
let exec = this.db
if (action) {
exec = exec.action(action)
}
exec.collection(this.collection).where({
_id: dbCmd.in(ids)
}).remove().then((res) => {
callback && callback(res.result)
if (this.pageData === pageMode.replace) {
this.refresh()
} else {
this.removeData(ids)
}
}).catch((err) => {
uni.showModal({
content: err.message,
showCancel: false
})
}).finally(() => {
uni.hideLoading()
})
},
removeData(ids) {
let il = ids.slice(0)
let dl = this.listData
for (let i = dl.length - 1; i >= 0; i--) {
let index = il.indexOf(dl[i]._id)
if (index >= 0) {
dl.splice(i, 1)
il.splice(index, 1)
}
}
},
_dispatchEvent(type, data) {
if (this._changeDataFunction) {
this._changeDataFunction(data, this._isEnded)
} else {
this.$emit(type, data, this._isEnded)
}
}
}
}

View File

@ -0,0 +1,853 @@
<template>
<view class="uni-data-checklist" :style="{'margin-top':isTop+'px'}">
<template v-if="!isLocal">
<view class="uni-data-loading">
<uni-load-more v-if="!mixinDatacomErrorMessage" status="loading" iconType="snow" :iconSize="18"
:content-text="contentText"></uni-load-more>
<text v-else>{{mixinDatacomErrorMessage}}</text>
</view>
</template>
<template v-else>
<checkbox-group v-if="multiple" class="checklist-group" :class="{'is-list':mode==='list' || wrap}"
@change="change">
<label class="checklist-box"
:class="['is--'+mode,item.selected?'is-checked':'',(disabled || !!item.disabled)?'is-disable':'',index!==0&&mode==='list'?'is-list-border':'']"
:style="item.styleBackgroud" v-for="(item,index) in dataList" :key="index">
<checkbox class="hidden" hidden :disabled="disabled || !!item.disabled" :value="item[map.value]+''"
:checked="item.selected" />
<view v-if="(mode !=='tag' && mode !== 'list') || ( mode === 'list' && icon === 'left')"
class="checkbox__inner" :style="item.styleIcon">
<view class="checkbox__inner-icon"></view>
</view>
<view class="checklist-content" :class="{'list-content':mode === 'list' && icon ==='left'}">
<text class="checklist-text" :style="item.styleIconText">{{item[map.text]}}</text>
<view v-if="mode === 'list' && icon === 'right'" class="checkobx__list" :style="item.styleBackgroud"></view>
</view>
</label>
</checkbox-group>
<radio-group v-else class="checklist-group" :class="{'is-list':mode==='list','is-wrap':wrap}" @change="change">
<label class="checklist-box"
:class="['is--'+mode,item.selected?'is-checked':'',(disabled || !!item.disabled)?'is-disable':'',index!==0&&mode==='list'?'is-list-border':'']"
:style="item.styleBackgroud" v-for="(item,index) in dataList" :key="index">
<radio class="hidden" hidden :disabled="disabled || item.disabled" :value="item[map.value]+''"
:checked="item.selected" />
<view v-if="(mode !=='tag' && mode !== 'list') || ( mode === 'list' && icon === 'left')" class="radio__inner"
:style="item.styleBackgroud">
<view class="radio__inner-icon" :style="item.styleIcon"></view>
</view>
<view class="checklist-content" :class="{'list-content':mode === 'list' && icon ==='left'}">
<text class="checklist-text" :style="item.styleIconText">{{item[map.text]}}</text>
<view v-if="mode === 'list' && icon === 'right'" :style="item.styleRightIcon" class="checkobx__list"></view>
</view>
</label>
</radio-group>
</template>
</view>
</template>
<script>
/**
* DataChecklist 数据选择器
* @description 通过数据渲染 checkbox radio
* @tutorial https://ext.dcloud.net.cn/plugin?id=xxx
* @property {String} mode = [default| list | button | tag] 显示模式
* @value default 默认横排模式
* @value list 列表模式
* @value button 按钮模式
* @value tag 标签模式
* @property {Boolean} multiple = [true|false] 是否多选
* @property {Array|String|Number} value 默认值
* @property {Array} localdata 本地数据 格式 [{text:'',value:''}]
* @property {Number|String} min 最小选择个数 multiple为true时生效
* @property {Number|String} max 最大选择个数 multiple为true时生效
* @property {Boolean} wrap 是否换行显示
* @property {String} icon = [left|right] list 列表模式下icon显示位置
* @property {Boolean} selectedColor 选中颜色
* @property {Boolean} emptyText 没有数据时显示的文字 本地数据无效
* @property {Boolean} selectedTextColor 选中文本颜色如不填写则自动显示
* @property {Object} map 字段映射 默认 map={text:'text',value:'value'}
* @value left 左侧显示
* @value right 右侧显示
* @event {Function} change 选中发生变化触发
*/
export default {
name: 'uniDataChecklist',
mixins: [uniCloud.mixinDatacom || {}],
emits: ['input', 'update:modelValue', 'change'],
props: {
mode: {
type: String,
default: 'default'
},
multiple: {
type: Boolean,
default: false
},
value: {
type: [Array, String, Number],
default () {
return ''
}
},
// TODO vue3
modelValue: {
type: [Array, String, Number],
default () {
return '';
}
},
localdata: {
type: Array,
default () {
return []
}
},
min: {
type: [Number, String],
default: ''
},
max: {
type: [Number, String],
default: ''
},
wrap: {
type: Boolean,
default: false
},
icon: {
type: String,
default: 'left'
},
selectedColor: {
type: String,
default: ''
},
selectedTextColor: {
type: String,
default: ''
},
emptyText: {
type: String,
default: '暂无数据'
},
disabled: {
type: Boolean,
default: false
},
map: {
type: Object,
default () {
return {
text: 'text',
value: 'value'
}
}
}
},
watch: {
localdata: {
handler(newVal) {
this.range = newVal
this.dataList = this.getDataList(this.getSelectedValue(newVal))
},
deep: true
},
mixinDatacomResData(newVal) {
this.range = newVal
this.dataList = this.getDataList(this.getSelectedValue(newVal))
},
value(newVal) {
this.dataList = this.getDataList(newVal)
// fix by mehaotian is_reset uni-forms
// if(!this.is_reset){
// this.is_reset = false
// this.formItem && this.formItem.setValue(newVal)
// }
},
modelValue(newVal) {
this.dataList = this.getDataList(newVal);
// if(!this.is_reset){
// this.is_reset = false
// this.formItem && this.formItem.setValue(newVal)
// }
}
},
data() {
return {
dataList: [],
range: [],
contentText: {
contentdown: '查看更多',
contentrefresh: '加载中',
contentnomore: '没有更多'
},
isLocal: true,
styles: {
selectedColor: '#2979ff',
selectedTextColor: '#666',
},
isTop: 0
};
},
computed: {
dataValue() {
if (this.value === '') return this.modelValue
if (this.modelValue === '') return this.value
return this.value
}
},
created() {
// this.form = this.getForm('uniForms')
// this.formItem = this.getForm('uniFormsItem')
// this.formItem && this.formItem.setValue(this.value)
// if (this.formItem) {
// this.isTop = 6
// if (this.formItem.name) {
// // name,formData
// if(!this.is_reset){
// this.is_reset = false
// this.formItem.setValue(this.dataValue)
// }
// this.rename = this.formItem.name
// this.form.inputChildrens.push(this)
// }
// }
if (this.localdata && this.localdata.length !== 0) {
this.isLocal = true
this.range = this.localdata
this.dataList = this.getDataList(this.getSelectedValue(this.range))
} else {
if (this.collection) {
this.isLocal = false
this.loadData()
}
}
},
methods: {
loadData() {
this.mixinDatacomGet().then(res => {
this.mixinDatacomResData = res.result.data
if (this.mixinDatacomResData.length === 0) {
this.isLocal = false
this.mixinDatacomErrorMessage = this.emptyText
} else {
this.isLocal = true
}
}).catch(err => {
this.mixinDatacomErrorMessage = err.message
})
},
/**
* 获取父元素实例
*/
getForm(name = 'uniForms') {
let parent = this.$parent;
let parentName = parent.$options.name;
while (parentName !== name) {
parent = parent.$parent;
if (!parent) return false
parentName = parent.$options.name;
}
return parent;
},
change(e) {
const values = e.detail.value
let detail = {
value: [],
data: []
}
if (this.multiple) {
this.range.forEach(item => {
if (values.includes(item[this.map.value] + '')) {
detail.value.push(item[this.map.value])
detail.data.push(item)
}
})
} else {
const range = this.range.find(item => (item[this.map.value] + '') === values)
if (range) {
detail = {
value: range[this.map.value],
data: range
}
}
}
// this.formItem && this.formItem.setValue(detail.value)
// TODO vue2
this.$emit('input', detail.value);
// // TOTO vue3
this.$emit('update:modelValue', detail.value);
this.$emit('change', {
detail
})
if (this.multiple) {
// v-model
// if (this.value.length === 0) {
this.dataList = this.getDataList(detail.value, true)
// }
} else {
this.dataList = this.getDataList(detail.value)
}
},
/**
* 获取渲染的新数组
* @param {Object} value 选中内容
*/
getDataList(value) {
//
let dataList = JSON.parse(JSON.stringify(this.range))
let list = []
if (this.multiple) {
if (!Array.isArray(value)) {
value = []
}
} else {
if (Array.isArray(value) && value.length) {
value = value[0]
}
}
dataList.forEach((item, index) => {
item.disabled = item.disable || item.disabled || false
if (this.multiple) {
if (value.length > 0) {
let have = value.find(val => val === item[this.map.value])
item.selected = have !== undefined
} else {
item.selected = false
}
} else {
item.selected = value === item[this.map.value]
}
list.push(item)
})
return this.setRange(list)
},
/**
* 处理最大最小值
* @param {Object} list
*/
setRange(list) {
let selectList = list.filter(item => item.selected)
let min = Number(this.min) || 0
let max = Number(this.max) || ''
list.forEach((item, index) => {
if (this.multiple) {
if (selectList.length <= min) {
let have = selectList.find(val => val[this.map.value] === item[this.map.value])
if (have !== undefined) {
item.disabled = true
}
}
if (selectList.length >= max && max !== '') {
let have = selectList.find(val => val[this.map.value] === item[this.map.value])
if (have === undefined) {
item.disabled = true
}
}
}
this.setStyles(item, index)
list[index] = item
})
return list
},
/**
* 设置 class
* @param {Object} item
* @param {Object} index
*/
setStyles(item, index) {
//
item.styleBackgroud = this.setStyleBackgroud(item)
item.styleIcon = this.setStyleIcon(item)
item.styleIconText = this.setStyleIconText(item)
item.styleRightIcon = this.setStyleRightIcon(item)
},
/**
* 获取选中值
* @param {Object} range
*/
getSelectedValue(range) {
if (!this.multiple) return this.dataValue
let selectedArr = []
range.forEach((item) => {
if (item.selected) {
selectedArr.push(item[this.map.value])
}
})
return this.dataValue.length > 0 ? this.dataValue : selectedArr
},
/**
* 设置背景样式
*/
setStyleBackgroud(item) {
let styles = {}
let selectedColor = this.selectedColor ? this.selectedColor : '#2979ff'
if (this.selectedColor) {
if (this.mode !== 'list') {
styles['border-color'] = item.selected ? selectedColor : '#DCDFE6'
}
if (this.mode === 'tag') {
styles['background-color'] = item.selected ? selectedColor : '#f5f5f5'
}
}
let classles = ''
for (let i in styles) {
classles += `${i}:${styles[i]};`
}
return classles
},
setStyleIcon(item) {
let styles = {}
let classles = ''
if (this.selectedColor) {
let selectedColor = this.selectedColor ? this.selectedColor : '#2979ff'
styles['background-color'] = item.selected ? selectedColor : '#fff'
styles['border-color'] = item.selected ? selectedColor : '#DCDFE6'
if (!item.selected && item.disabled) {
styles['background-color'] = '#F2F6FC'
styles['border-color'] = item.selected ? selectedColor : '#DCDFE6'
}
}
for (let i in styles) {
classles += `${i}:${styles[i]};`
}
return classles
},
setStyleIconText(item) {
let styles = {}
let classles = ''
if (this.selectedColor) {
let selectedColor = this.selectedColor ? this.selectedColor : '#2979ff'
if (this.mode === 'tag') {
styles.color = item.selected ? (this.selectedTextColor ? this.selectedTextColor : '#fff') : '#666'
} else {
styles.color = item.selected ? (this.selectedTextColor ? this.selectedTextColor : selectedColor) : '#666'
}
if (!item.selected && item.disabled) {
styles.color = '#999'
}
}
for (let i in styles) {
classles += `${i}:${styles[i]};`
}
return classles
},
setStyleRightIcon(item) {
let styles = {}
let classles = ''
if (this.mode === 'list') {
styles['border-color'] = item.selected ? this.styles.selectedColor : '#DCDFE6'
}
for (let i in styles) {
classles += `${i}:${styles[i]};`
}
return classles
}
}
}
</script>
<style lang="scss">
$uni-primary: #2979ff !default;
$border-color: #DCDFE6;
$disable: 0.4;
@mixin flex {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
}
.uni-data-loading {
@include flex;
flex-direction: row;
justify-content: center;
align-items: center;
height: 36px;
padding-left: 10px;
color: #999;
}
.uni-data-checklist {
position: relative;
z-index: 0;
flex: 1;
//
.checklist-group {
@include flex;
flex-direction: row;
flex-wrap: wrap;
&.is-list {
flex-direction: column;
}
.checklist-box {
@include flex;
flex-direction: row;
align-items: center;
position: relative;
margin: 5px 0;
margin-right: 25px;
.hidden {
position: absolute;
opacity: 0;
}
//
.checklist-content {
@include flex;
flex: 1;
flex-direction: row;
align-items: center;
justify-content: space-between;
.checklist-text {
font-size: 14px;
color: #666;
margin-left: 5px;
line-height: 14px;
}
.checkobx__list {
border-right-width: 1px;
border-right-color: #007aff;
border-right-style: solid;
border-bottom-width: 1px;
border-bottom-color: #007aff;
border-bottom-style: solid;
height: 12px;
width: 6px;
left: -5px;
transform-origin: center;
transform: rotate(45deg);
opacity: 0;
}
}
//
.checkbox__inner {
/* #ifndef APP-NVUE */
flex-shrink: 0;
box-sizing: border-box;
/* #endif */
position: relative;
width: 16px;
height: 16px;
border: 1px solid $border-color;
border-radius: 4px;
background-color: #fff;
z-index: 1;
.checkbox__inner-icon {
position: absolute;
/* #ifdef APP-NVUE */
top: 2px;
/* #endif */
/* #ifndef APP-NVUE */
top: 1px;
/* #endif */
left: 5px;
height: 8px;
width: 4px;
border-right-width: 1px;
border-right-color: #fff;
border-right-style: solid;
border-bottom-width: 1px;
border-bottom-color: #fff;
border-bottom-style: solid;
opacity: 0;
transform-origin: center;
transform: rotate(40deg);
}
}
//
.radio__inner {
@include flex;
/* #ifndef APP-NVUE */
flex-shrink: 0;
box-sizing: border-box;
/* #endif */
justify-content: center;
align-items: center;
position: relative;
width: 16px;
height: 16px;
border: 1px solid $border-color;
border-radius: 16px;
background-color: #fff;
z-index: 1;
.radio__inner-icon {
width: 8px;
height: 8px;
border-radius: 10px;
opacity: 0;
}
}
//
&.is--default {
//
&.is-disable {
/* #ifdef H5 */
cursor: not-allowed;
/* #endif */
.checkbox__inner {
background-color: #F2F6FC;
border-color: $border-color;
/* #ifdef H5 */
cursor: not-allowed;
/* #endif */
}
.radio__inner {
background-color: #F2F6FC;
border-color: $border-color;
}
.checklist-text {
color: #999;
}
}
//
&.is-checked {
.checkbox__inner {
border-color: $uni-primary;
background-color: $uni-primary;
.checkbox__inner-icon {
opacity: 1;
transform: rotate(45deg);
}
}
.radio__inner {
border-color: $uni-primary;
.radio__inner-icon {
opacity: 1;
background-color: $uni-primary;
}
}
.checklist-text {
color: $uni-primary;
}
//
&.is-disable {
.checkbox__inner {
opacity: $disable;
}
.checklist-text {
opacity: $disable;
}
.radio__inner {
opacity: $disable;
}
}
}
}
//
&.is--button {
margin-right: 10px;
padding: 5px 10px;
border: 1px $border-color solid;
border-radius: 3px;
transition: border-color 0.2s;
//
&.is-disable {
/* #ifdef H5 */
cursor: not-allowed;
/* #endif */
border: 1px #eee solid;
opacity: $disable;
.checkbox__inner {
background-color: #F2F6FC;
border-color: $border-color;
/* #ifdef H5 */
cursor: not-allowed;
/* #endif */
}
.radio__inner {
background-color: #F2F6FC;
border-color: $border-color;
/* #ifdef H5 */
cursor: not-allowed;
/* #endif */
}
.checklist-text {
color: #999;
}
}
&.is-checked {
border-color: $uni-primary;
.checkbox__inner {
border-color: $uni-primary;
background-color: $uni-primary;
.checkbox__inner-icon {
opacity: 1;
transform: rotate(45deg);
}
}
.radio__inner {
border-color: $uni-primary;
.radio__inner-icon {
opacity: 1;
background-color: $uni-primary;
}
}
.checklist-text {
color: $uni-primary;
}
//
&.is-disable {
opacity: $disable;
}
}
}
//
&.is--tag {
margin-right: 10px;
padding: 5px 10px;
border: 1px $border-color solid;
border-radius: 3px;
background-color: #f5f5f5;
.checklist-text {
margin: 0;
color: #666;
}
//
&.is-disable {
/* #ifdef H5 */
cursor: not-allowed;
/* #endif */
opacity: $disable;
}
&.is-checked {
background-color: $uni-primary;
border-color: $uni-primary;
.checklist-text {
color: #fff;
}
}
}
//
&.is--list {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
padding: 10px 15px;
padding-left: 0;
margin: 0;
&.is-list-border {
border-top: 1px #eee solid;
}
//
&.is-disable {
/* #ifdef H5 */
cursor: not-allowed;
/* #endif */
.checkbox__inner {
background-color: #F2F6FC;
border-color: $border-color;
/* #ifdef H5 */
cursor: not-allowed;
/* #endif */
}
.checklist-text {
color: #999;
}
}
&.is-checked {
.checkbox__inner {
border-color: $uni-primary;
background-color: $uni-primary;
.checkbox__inner-icon {
opacity: 1;
transform: rotate(45deg);
}
}
.radio__inner {
border-color: $uni-primary;
.radio__inner-icon {
opacity: 1;
background-color: $uni-primary;
}
}
.checklist-text {
color: $uni-primary;
}
.checklist-content {
.checkobx__list {
opacity: 1;
border-color: $uni-primary;
}
}
//
&.is-disable {
.checkbox__inner {
opacity: $disable;
}
.checklist-text {
opacity: $disable;
}
}
}
}
}
}
}
</style>

View File

@ -0,0 +1,87 @@
{
"id": "uni-data-checkbox",
"displayName": "uni-data-checkbox 数据选择器",
"version": "1.0.6",
"description": "通过数据驱动的单选框和复选框",
"keywords": [
"uni-ui",
"checkbox",
"单选",
"多选",
"单选多选"
],
"repository": "https://github.com/dcloudio/uni-ui",
"engines": {
"HBuilderX": "^3.1.1"
},
"directories": {
"example": "../../temps/example_temps"
},
"dcloudext": {
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": ""
},
"declaration": {
"ads": "无",
"data": "无",
"permissions": "无"
},
"npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui",
"type": "component-vue"
},
"uni_modules": {
"dependencies": ["uni-load-more","uni-scss"],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "y",
"aliyun": "y",
"alipay": "n"
},
"client": {
"App": {
"app-vue": "y",
"app-nvue": "y",
"app-harmony": "u",
"app-uvue": "u"
},
"H5-mobile": {
"Safari": "y",
"Android Browser": "y",
"微信浏览器(Android)": "y",
"QQ浏览器(Android)": "y"
},
"H5-pc": {
"Chrome": "y",
"IE": "y",
"Edge": "y",
"Firefox": "y",
"Safari": "y"
},
"小程序": {
"微信": "y",
"阿里": "y",
"百度": "y",
"字节跳动": "y",
"QQ": "y"
},
"快应用": {
"华为": "u",
"联盟": "u"
},
"Vue": {
"vue2": "y",
"vue3": "y"
}
}
}
}
}

View File

@ -0,0 +1,18 @@
## DataCheckbox 数据驱动的单选复选框
> **组件名uni-data-checkbox**
> 代码块: `uDataCheckbox`
本组件是基于uni-app基础组件checkbox的封装。本组件要解决问题包括
1. 数据绑定型组件给本组件绑定一个data会自动渲染一组候选内容。再以往开发者需要编写不少代码实现类似功能
2. 自动的表单校验组件绑定了data且符合[uni-forms](https://ext.dcloud.net.cn/plugin?id=2773)组件的表单校验规范,搭配使用会自动实现表单校验
3. 本组件合并了单选多选
4. 本组件有若干风格选择如普通的单选多选框、并列button风格、tag风格。开发者可以快速选择需要的风格。但作为一个封装组件样式代码虽然不用自己写了却会牺牲一定的样式自定义性
在uniCloud开发中`DB Schema`中配置了enum枚举等类型后在web控制台的[自动生成表单](https://uniapp.dcloud.io/uniCloud/schema?id=autocode)功能中,会自动生成``uni-data-checkbox``组件并绑定好data
### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-data-checkbox)
#### 如使用过程中有任何问题或者您对uni-ui有一些好的建议欢迎加入 uni-ui 交流群871950839

View File

@ -0,0 +1,168 @@
## 2.2.382024-10-15
- 修复 微信小程序中的getSystemInfo警告
## 2.2.372024-10-12
- 修复 微信小程序中的getSystemInfo警告
## 2.2.362024-10-12
- 修复 微信小程序中的getSystemInfo警告
## 2.2.352024-09-21
- 修复 没有选中日期时点击确定直接报错的Bug [详情](https://ask.dcloud.net.cn/question/198168)
## 2.2.342024-04-24
- 新增 日期点击事件,在点击日期时会触发该事件。
## 2.2.332024-04-15
- 修复 抖音小程序事件传递失效bug
## 2.2.322024-02-20
- 修复 日历的close事件触发异常的bug [详情](https://github.com/dcloudio/uni-ui/issues/844)
## 2.2.312024-02-20
- 修复 h5平台 右边日历的月份默认+1的bug [详情](https://github.com/dcloudio/uni-ui/issues/841)
## 2.2.302024-01-31
- 修复 隐藏“秒”时在IOS15及以下版本时出现 结束时间在开始时间之前 的bug [详情](https://github.com/dcloudio/uni-ui/issues/788)
## 2.2.292024-01-20
- 新增 show事件弹窗弹出时触发该事件 [详情](https://github.com/dcloudio/uni-app/issues/4694)
## 2.2.282024-01-18
- 去除 noChange事件当进行日期范围选择时若只选了一天则开始结束日期都为同一天 [详情](https://github.com/dcloudio/uni-ui/issues/815)
## 2.2.272024-01-10
- 优化 增加noChange事件当进行日期范围选择时若有空值则触发该事件 [详情](https://github.com/dcloudio/uni-ui/issues/815)
## 2.2.262024-01-08
- 修复 字节小程序时间选择范围器失效问题 [详情](https://github.com/dcloudio/uni-ui/issues/834)
## 2.2.252023-10-18
- 修复 PC端初次修改时间开始时间未更新的Bug [详情](https://github.com/dcloudio/uni-ui/issues/737)
## 2.2.242023-06-02
- 修复 部分情况修改时间开始、结束时间显示异常的Bug [详情](https://ask.dcloud.net.cn/question/171146)
- 优化 当前月可以选择上月、下月的日期的Bug
## 2.2.232023-05-02
- 修复 部分情况修改时间开始时间未更新的Bug [详情](https://github.com/dcloudio/uni-ui/issues/737)
- 修复 部分平台及设备第一次点击无法显示弹框的Bug
- 修复 ios 日期格式未补零显示及使用异常的Bug [详情](https://ask.dcloud.net.cn/question/162979)
## 2.2.222023-03-30
- 修复 日历 picker 修改年月后自动选中当月1日的Bug [详情](https://ask.dcloud.net.cn/question/165937)
- 修复 小程序端 低版本 ios NaN的Bug [详情](https://ask.dcloud.net.cn/question/162979)
## 2.2.212023-02-20
- 修复 firefox 浏览器显示区域点击无法拉起日历弹框的Bug [详情](https://ask.dcloud.net.cn/question/163362)
## 2.2.202023-02-17
- 优化 值为空依然选中当天问题
- 优化 提供 default-value 属性支持配置选择器打开时默认显示的时间
- 优化 非范围选择未选择日期时间,点击确认按钮选中当前日期时间
- 优化 字节小程序日期时间范围选择底部日期换行的Bug
## 2.2.192023-02-09
- 修复 2.2.18 引起范围选择配置 end 选择无效的Bug [详情](https://github.com/dcloudio/uni-ui/issues/686)
## 2.2.182023-02-08
- 修复 移动端范围选择change事件触发异常的Bug [详情](https://github.com/dcloudio/uni-ui/issues/684)
- 优化 PC端输入日期格式错误时返回当前日期时间
- 优化 PC端输入日期时间超出 start、end 限制的Bug
- 优化 移动端日期时间范围用法时间展示不完整问题
## 2.2.172023-02-04
- 修复 小程序端绑定 Date 类型报错的Bug [详情](https://github.com/dcloudio/uni-ui/issues/679)
- 修复 vue3 time-picker 无法显示绑定时分秒的Bug
## 2.2.162023-02-02
- 修复 字节小程序报错的Bug
## 2.2.152023-02-02
- 修复 某些情况切换月份错误的Bug
## 2.2.142023-01-30
- 修复 某些情况切换月份错误的Bug [详情](https://ask.dcloud.net.cn/question/162033)
## 2.2.132023-01-10
- 修复 多次加载组件造成内存占用的Bug
## 2.2.122022-12-01
- 修复 vue3 下 i18n 国际化初始值不正确的Bug
## 2.2.112022-09-19
- 修复 支付宝小程序样式错乱的Bug [详情](https://github.com/dcloudio/uni-app/issues/3861)
## 2.2.102022-09-19
- 修复 反向选择日期范围日期显示异常的Bug [详情](https://ask.dcloud.net.cn/question/153401?item_id=212892&rf=false)
## 2.2.92022-09-16
- 可以使用 uni-scss 控制主题色
## 2.2.82022-09-08
- 修复 close事件无效的Bug
## 2.2.72022-09-05
- 修复 移动端 maskClick 无效的Bug [详情](https://ask.dcloud.net.cn/question/140824)
## 2.2.62022-06-30
- 优化 组件样式调整了组件图标大小、高度、颜色等与uni-ui风格保持一致
## 2.2.52022-06-24
- 修复 日历顶部年月及底部确认未国际化的Bug
## 2.2.42022-03-31
- 修复 Vue3 下动态赋值,单选类型未响应的Bug
## 2.2.32022-03-28
- 修复 Vue3 下动态赋值未响应的Bug
## 2.2.22021-12-10
- 修复 clear-icon 属性在小程序平台不生效的Bug
## 2.2.12021-12-10
- 修复 日期范围选在小程序平台必须多点击一次才能取消选中状态的Bug
## 2.2.02021-11-19
- 优化 组件UI并提供设计资源 [详情](https://uniapp.dcloud.io/component/uniui/resource)
- 文档迁移 [https://uniapp.dcloud.io/component/uniui/uni-datetime-picker](https://uniapp.dcloud.io/component/uniui/uni-datetime-picker)
## 2.1.52021-11-09
- 新增 提供组件设计资源,组件样式调整
## 2.1.42021-09-10
- 修复 hide-second 在移动端的Bug
- 修复 单选赋默认值时赋值日期未高亮的Bug
- 修复 赋默认值时移动端未正确显示时间的Bug
## 2.1.32021-09-09
- 新增 hide-second 属性,支持只使用时分,隐藏秒
## 2.1.22021-09-03
- 优化 取消选中时(范围选)直接开始下一次选择, 避免多点一次
- 优化 移动端支持清除按钮,同时支持通过 ref 调用组件的 clear 方法
- 优化 调整字号大小,美化日历界面
- 修复 因国际化导致的 placeholder 失效的Bug
## 2.1.12021-08-24
- 新增 支持国际化
- 优化 范围选择器在 pc 端过宽的问题
## 2.1.02021-08-09
- 新增 适配 vue3
## 2.0.192021-08-09
- 新增 支持作为 uni-forms 子组件相关功能
- 修复 在 uni-forms 中使用时,选择时间报 NAN 错误的Bug
## 2.0.182021-08-05
- 修复 type 属性动态赋值无效的Bug
- 修复 ‘确认’按钮被 tabbar 遮盖 bug
- 修复 组件未赋值时范围选左、右日历相同的Bug
## 2.0.172021-08-04
- 修复 范围选未正确显示当前值的Bug
- 修复 h5 平台(移动端)报错 'cale' of undefined 的Bug
## 2.0.162021-07-21
- 新增 return-type 属性支持返回 date 日期对象
## 2.0.152021-07-14
- 修复 单选日期类型初始赋值后不在当前日历的Bug
- 新增 clearIcon 属性,显示框的清空按钮可配置显示隐藏(仅 pc 有效)
- 优化 移动端移除显示框的清空按钮,无实际用途
## 2.0.142021-07-14
- 修复 组件赋值为空界面未更新的Bug
- 修复 start 和 end 不能动态赋值的Bug
- 修复 范围选类型用户选择后再次选择右侧日历结束日期显示不正确的Bug
## 2.0.132021-07-08
- 修复 范围选择不能动态赋值的Bug
## 2.0.122021-07-08
- 修复 范围选择的初始时间在一个月内时造成无法选择的bug
## 2.0.112021-07-08
- 优化 弹出层在超出视窗边缘定位不准确的问题
## 2.0.102021-07-08
- 修复 范围起始点样式的背景色与今日样式的字体前景色融合导致日期字体看不清的Bug
- 优化 弹出层在超出视窗边缘被遮盖的问题
## 2.0.92021-07-07
- 新增 maskClick 事件
- 修复 特殊情况日历 rpx 布局错误的Bugrpx -> px
- 修复 范围选择时清空返回值不合理的bug['', ''] -> []
## 2.0.82021-07-07
- 新增 日期时间显示框支持插槽
## 2.0.72021-07-01
- 优化 添加 uni-icons 依赖
## 2.0.62021-05-22
- 修复 图标在小程序上不显示的Bug
- 优化 重命名引用组件,避免潜在组件命名冲突
## 2.0.52021-05-20
- 优化 代码目录扁平化
## 2.0.42021-05-12
- 新增 组件示例地址
## 2.0.32021-05-10
- 修复 ios 下不识别 '-' 日期格式的Bug
- 优化 pc 下弹出层添加边框和阴影
## 2.0.22021-05-08
- 修复 在 admin 中获取弹出层定位错误的bug
## 2.0.12021-05-08
- 修复 type 属性向下兼容,默认值从 date 变更为 datetime
## 2.0.02021-04-30
- 支持日历形式的日期+时间的范围选择
> 注意此版本不向后兼容不再支持单独时间选择type=time及相关的 hide-second 属性(时间选可使用内置组件 picker
## 1.0.62021-03-18
- 新增 hide-second 属性,时间支持仅选择时、分
- 修复 选择跟显示的日期不一样的Bug
- 修复 chang事件触发2次的Bug
- 修复 分、秒 end 范围错误的Bug
- 优化 更好的 nvue 适配

View File

@ -0,0 +1,177 @@
<template>
<view class="uni-calendar-item__weeks-box" :class="{
'uni-calendar-item--disable':weeks.disable,
'uni-calendar-item--before-checked-x':weeks.beforeMultiple,
'uni-calendar-item--multiple': weeks.multiple,
'uni-calendar-item--after-checked-x':weeks.afterMultiple,
}" @click="choiceDate(weeks)" @mouseenter="handleMousemove(weeks)">
<view class="uni-calendar-item__weeks-box-item" :class="{
'uni-calendar-item--checked':calendar.fullDate === weeks.fullDate && (calendar.userChecked || !checkHover),
'uni-calendar-item--checked-range-text': checkHover,
'uni-calendar-item--before-checked':weeks.beforeMultiple,
'uni-calendar-item--multiple': weeks.multiple,
'uni-calendar-item--after-checked':weeks.afterMultiple,
'uni-calendar-item--disable':weeks.disable,
}">
<text v-if="selected && weeks.extraInfo" class="uni-calendar-item__weeks-box-circle"></text>
<text class="uni-calendar-item__weeks-box-text uni-calendar-item__weeks-box-text-disable uni-calendar-item--checked-text">{{weeks.date}}</text>
</view>
<view :class="{'uni-calendar-item--today': weeks.isToday}"></view>
</view>
</template>
<script>
export default {
props: {
weeks: {
type: Object,
default () {
return {}
}
},
calendar: {
type: Object,
default: () => {
return {}
}
},
selected: {
type: Array,
default: () => {
return []
}
},
checkHover: {
type: Boolean,
default: false
}
},
methods: {
choiceDate(weeks) {
this.$emit('change', weeks)
},
handleMousemove(weeks) {
this.$emit('handleMouse', weeks)
}
}
}
</script>
<style lang="scss" >
$uni-primary: #007aff !default;
.uni-calendar-item__weeks-box {
flex: 1;
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: column;
justify-content: center;
align-items: center;
margin: 1px 0;
position: relative;
}
.uni-calendar-item__weeks-box-text {
font-size: 14px;
// font-family: Lato-Bold, Lato;
font-weight: bold;
color: darken($color: $uni-primary, $amount: 40%);
}
.uni-calendar-item__weeks-box-item {
position: relative;
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: column;
justify-content: center;
align-items: center;
width: 40px;
height: 40px;
/* #ifdef H5 */
cursor: pointer;
/* #endif */
}
.uni-calendar-item__weeks-box-circle {
position: absolute;
top: 5px;
right: 5px;
width: 8px;
height: 8px;
border-radius: 8px;
background-color: #dd524d;
}
.uni-calendar-item__weeks-box .uni-calendar-item--disable {
cursor: default;
}
.uni-calendar-item--disable .uni-calendar-item__weeks-box-text-disable {
color: #D1D1D1;
}
.uni-calendar-item--today {
position: absolute;
top: 10px;
right: 17%;
background-color: #dd524d;
width:6px;
height: 6px;
border-radius: 50%;
}
.uni-calendar-item--extra {
color: #dd524d;
opacity: 0.8;
}
.uni-calendar-item__weeks-box .uni-calendar-item--checked {
background-color: $uni-primary;
border-radius: 50%;
box-sizing: border-box;
border: 3px solid #fff;
}
.uni-calendar-item--checked .uni-calendar-item--checked-text {
color: #fff;
}
.uni-calendar-item--multiple .uni-calendar-item--checked-range-text {
color: #333;
}
.uni-calendar-item--multiple {
background-color: #F6F7FC;
// color: #fff;
}
.uni-calendar-item--multiple .uni-calendar-item--before-checked,
.uni-calendar-item--multiple .uni-calendar-item--after-checked {
background-color: $uni-primary;
border-radius: 50%;
box-sizing: border-box;
border: 3px solid #F6F7FC;
}
.uni-calendar-item--before-checked .uni-calendar-item--checked-text,
.uni-calendar-item--after-checked .uni-calendar-item--checked-text {
color: #fff;
}
.uni-calendar-item--before-checked-x {
border-top-left-radius: 50px;
border-bottom-left-radius: 50px;
box-sizing: border-box;
background-color: #F6F7FC;
}
.uni-calendar-item--after-checked-x {
border-top-right-radius: 50px;
border-bottom-right-radius: 50px;
background-color: #F6F7FC;
}
</style>

View File

@ -0,0 +1,947 @@
<template>
<view class="uni-calendar" @mouseleave="leaveCale">
<view v-if="!insert && show" class="uni-calendar__mask" :class="{'uni-calendar--mask-show':aniMaskShow}"
@click="maskClick"></view>
<view v-if="insert || show" class="uni-calendar__content"
:class="{'uni-calendar--fixed':!insert,'uni-calendar--ani-show':aniMaskShow, 'uni-calendar__content-mobile': aniMaskShow}">
<view class="uni-calendar__header" :class="{'uni-calendar__header-mobile' :!insert}">
<view class="uni-calendar__header-btn-box" @click.stop="changeMonth('pre')">
<view class="uni-calendar__header-btn uni-calendar--left"></view>
</view>
<picker mode="date" :value="date" fields="month" @change="bindDateChange">
<text
class="uni-calendar__header-text">{{ (nowDate.year||'') + yearText + ( nowDate.month||'') + monthText}}</text>
</picker>
<view class="uni-calendar__header-btn-box" @click.stop="changeMonth('next')">
<view class="uni-calendar__header-btn uni-calendar--right"></view>
</view>
<view v-if="!insert" class="dialog-close" @click="maskClick">
<view class="dialog-close-plus" data-id="close"></view>
<view class="dialog-close-plus dialog-close-rotate" data-id="close"></view>
</view>
</view>
<view class="uni-calendar__box">
<view v-if="showMonth" class="uni-calendar__box-bg">
<text class="uni-calendar__box-bg-text">{{nowDate.month}}</text>
</view>
<view class="uni-calendar__weeks" style="padding-bottom: 7px;">
<view class="uni-calendar__weeks-day">
<text class="uni-calendar__weeks-day-text">{{SUNText}}</text>
</view>
<view class="uni-calendar__weeks-day">
<text class="uni-calendar__weeks-day-text">{{MONText}}</text>
</view>
<view class="uni-calendar__weeks-day">
<text class="uni-calendar__weeks-day-text">{{TUEText}}</text>
</view>
<view class="uni-calendar__weeks-day">
<text class="uni-calendar__weeks-day-text">{{WEDText}}</text>
</view>
<view class="uni-calendar__weeks-day">
<text class="uni-calendar__weeks-day-text">{{THUText}}</text>
</view>
<view class="uni-calendar__weeks-day">
<text class="uni-calendar__weeks-day-text">{{FRIText}}</text>
</view>
<view class="uni-calendar__weeks-day">
<text class="uni-calendar__weeks-day-text">{{SATText}}</text>
</view>
</view>
<view class="uni-calendar__weeks" v-for="(item,weekIndex) in weeks" :key="weekIndex">
<view class="uni-calendar__weeks-item" v-for="(weeks,weeksIndex) in item" :key="weeksIndex">
<calendar-item class="uni-calendar-item--hook" :weeks="weeks" :calendar="calendar" :selected="selected"
:checkHover="range" @change="choiceDate" @handleMouse="handleMouse">
</calendar-item>
</view>
</view>
</view>
<view v-if="!insert && !range && hasTime" class="uni-date-changed uni-calendar--fixed-top"
style="padding: 0 80px;">
<view class="uni-date-changed--time-date">{{tempSingleDate ? tempSingleDate : selectDateText}}</view>
<time-picker type="time" :start="timepickerStartTime" :end="timepickerEndTime" v-model="time"
:disabled="!tempSingleDate" :border="false" :hide-second="hideSecond" class="time-picker-style">
</time-picker>
</view>
<view v-if="!insert && range && hasTime" class="uni-date-changed uni-calendar--fixed-top">
<view class="uni-date-changed--time-start">
<view class="uni-date-changed--time-date">{{tempRange.before ? tempRange.before : startDateText}}
</view>
<time-picker type="time" :start="timepickerStartTime" v-model="timeRange.startTime" :border="false"
:hide-second="hideSecond" :disabled="!tempRange.before" class="time-picker-style">
</time-picker>
</view>
<view style="line-height: 50px;">
<uni-icons type="arrowthinright" color="#999"></uni-icons>
</view>
<view class="uni-date-changed--time-end">
<view class="uni-date-changed--time-date">{{tempRange.after ? tempRange.after : endDateText}}</view>
<time-picker type="time" :end="timepickerEndTime" v-model="timeRange.endTime" :border="false"
:hide-second="hideSecond" :disabled="!tempRange.after" class="time-picker-style">
</time-picker>
</view>
</view>
<view v-if="!insert" class="uni-date-changed uni-date-btn--ok">
<view class="uni-datetime-picker--btn" @click="confirm">{{confirmText}}</view>
</view>
</view>
</view>
</template>
<script>
import {
Calendar,
getDate,
getTime
} from './util.js';
import calendarItem from './calendar-item.vue'
import timePicker from './time-picker.vue'
import {
initVueI18n
} from '@dcloudio/uni-i18n'
import i18nMessages from './i18n/index.js'
const {
t
} = initVueI18n(i18nMessages)
/**
* Calendar 日历
* @description 日历组件可以查看日期选择任意范围内的日期打点操作常用场景如酒店日期预订火车机票选择购买日期上下班打卡等
* @tutorial https://ext.dcloud.net.cn/plugin?id=56
* @property {String} date 自定义当前时间默认为今天
* @property {String} startDate 日期选择范围-开始日期
* @property {String} endDate 日期选择范围-结束日期
* @property {Boolean} range 范围选择
* @property {Boolean} insert = [true|false] 插入模式,默认为false
* @value true 弹窗模式
* @value false 插入模式
* @property {Boolean} clearDate = [true|false] 弹窗模式是否清空上次选择内容
* @property {Array} selected 打点期待格式[{date: '2019-06-27', info: '签到', data: { custom: '自定义信息', name: '自定义消息头',xxx:xxx... }}]
* @property {Boolean} showMonth 是否选择月份为背景
* @property {[String} defaultValue 选择器打开时默认显示的时间
* @event {Function} change 日期改变`insert :ture` 时生效
* @event {Function} confirm 确认选择`insert :false` 时生效
* @event {Function} monthSwitch 切换月份时触发
* @example <uni-calendar :insert="true" :start-date="'2019-3-2'":end-date="'2019-5-20'"@change="change" />
*/
export default {
components: {
calendarItem,
timePicker
},
options: {
// #ifdef MP-TOUTIAO
virtualHost: false,
// #endif
// #ifndef MP-TOUTIAO
virtualHost: true
// #endif
},
props: {
date: {
type: String,
default: ''
},
defTime: {
type: [String, Object],
default: ''
},
selectableTimes: {
type: [Object],
default () {
return {}
}
},
selected: {
type: Array,
default () {
return []
}
},
startDate: {
type: String,
default: ''
},
endDate: {
type: String,
default: ''
},
startPlaceholder: {
type: String,
default: ''
},
endPlaceholder: {
type: String,
default: ''
},
range: {
type: Boolean,
default: false
},
hasTime: {
type: Boolean,
default: false
},
insert: {
type: Boolean,
default: true
},
showMonth: {
type: Boolean,
default: true
},
clearDate: {
type: Boolean,
default: true
},
checkHover: {
type: Boolean,
default: true
},
hideSecond: {
type: [Boolean],
default: false
},
pleStatus: {
type: Object,
default () {
return {
before: '',
after: '',
data: [],
fulldate: ''
}
}
},
defaultValue: {
type: [String, Object, Array],
default: ''
}
},
data() {
return {
show: false,
weeks: [],
calendar: {},
nowDate: {},
aniMaskShow: false,
firstEnter: true,
time: '',
timeRange: {
startTime: '',
endTime: ''
},
tempSingleDate: '',
tempRange: {
before: '',
after: ''
}
}
},
watch: {
date: {
immediate: true,
handler(newVal) {
if (!this.range) {
this.tempSingleDate = newVal
setTimeout(() => {
this.init(newVal)
}, 100)
}
}
},
defTime: {
immediate: true,
handler(newVal) {
if (!this.range) {
this.time = newVal
} else {
this.timeRange.startTime = newVal.start
this.timeRange.endTime = newVal.end
}
}
},
startDate(val) {
// watch created
if (!this.cale) {
return
}
this.cale.setStartDate(val)
this.cale.setDate(this.nowDate.fullDate)
this.weeks = this.cale.weeks
},
endDate(val) {
// watch created
if (!this.cale) {
return
}
this.cale.setEndDate(val)
this.cale.setDate(this.nowDate.fullDate)
this.weeks = this.cale.weeks
},
selected(newVal) {
// watch created
if (!this.cale) {
return
}
this.cale.setSelectInfo(this.nowDate.fullDate, newVal)
this.weeks = this.cale.weeks
},
pleStatus: {
immediate: true,
handler(newVal) {
const {
before,
after,
fulldate,
which
} = newVal
this.tempRange.before = before
this.tempRange.after = after
setTimeout(() => {
if (fulldate) {
this.cale.setHoverMultiple(fulldate)
if (before && after) {
this.cale.lastHover = true
if (this.rangeWithinMonth(after, before)) return
this.setDate(before)
} else {
this.cale.setMultiple(fulldate)
this.setDate(this.nowDate.fullDate)
this.calendar.fullDate = ''
this.cale.lastHover = false
}
} else {
// watch created
if (!this.cale) {
return
}
this.cale.setDefaultMultiple(before, after)
if (which === 'left' && before) {
this.setDate(before)
this.weeks = this.cale.weeks
} else if (after) {
this.setDate(after)
this.weeks = this.cale.weeks
}
this.cale.lastHover = true
}
}, 16)
}
}
},
computed: {
timepickerStartTime() {
const activeDate = this.range ? this.tempRange.before : this.calendar.fullDate
return activeDate === this.startDate ? this.selectableTimes.start : ''
},
timepickerEndTime() {
const activeDate = this.range ? this.tempRange.after : this.calendar.fullDate
return activeDate === this.endDate ? this.selectableTimes.end : ''
},
/**
* for i18n
*/
selectDateText() {
return t("uni-datetime-picker.selectDate")
},
startDateText() {
return this.startPlaceholder || t("uni-datetime-picker.startDate")
},
endDateText() {
return this.endPlaceholder || t("uni-datetime-picker.endDate")
},
okText() {
return t("uni-datetime-picker.ok")
},
yearText() {
return t("uni-datetime-picker.year")
},
monthText() {
return t("uni-datetime-picker.month")
},
MONText() {
return t("uni-calender.MON")
},
TUEText() {
return t("uni-calender.TUE")
},
WEDText() {
return t("uni-calender.WED")
},
THUText() {
return t("uni-calender.THU")
},
FRIText() {
return t("uni-calender.FRI")
},
SATText() {
return t("uni-calender.SAT")
},
SUNText() {
return t("uni-calender.SUN")
},
confirmText() {
return t("uni-calender.confirm")
},
},
created() {
//
this.cale = new Calendar({
selected: this.selected,
startDate: this.startDate,
endDate: this.endDate,
range: this.range,
})
//
this.init(this.date)
},
methods: {
leaveCale() {
this.firstEnter = true
},
handleMouse(weeks) {
if (weeks.disable) return
if (this.cale.lastHover) return
let {
before,
after
} = this.cale.multipleStatus
if (!before) return
this.calendar = weeks
//
this.cale.setHoverMultiple(this.calendar.fullDate)
this.weeks = this.cale.weeks
// hover
if (this.firstEnter) {
this.$emit('firstEnterCale', this.cale.multipleStatus)
this.firstEnter = false
}
},
rangeWithinMonth(A, B) {
const [yearA, monthA] = A.split('-')
const [yearB, monthB] = B.split('-')
return yearA === yearB && monthA === monthB
},
//
maskClick() {
this.close()
this.$emit('maskClose')
},
clearCalender() {
if (this.range) {
this.timeRange.startTime = ''
this.timeRange.endTime = ''
this.tempRange.before = ''
this.tempRange.after = ''
this.cale.multipleStatus.before = ''
this.cale.multipleStatus.after = ''
this.cale.multipleStatus.data = []
this.cale.lastHover = false
} else {
this.time = ''
this.tempSingleDate = ''
}
this.calendar.fullDate = ''
this.setDate(new Date())
},
bindDateChange(e) {
const value = e.detail.value + '-1'
this.setDate(value)
},
/**
* 初始化日期显示
* @param {Object} date
*/
init(date) {
// watch created
if (!this.cale) {
return
}
this.cale.setDate(date || new Date())
this.weeks = this.cale.weeks
this.nowDate = this.cale.getInfo(date)
this.calendar = {
...this.nowDate
}
if (!date) {
// date
this.calendar.fullDate = ''
if (this.defaultValue && !this.range) {
//
const defaultDate = new Date(this.defaultValue)
const fullDate = getDate(defaultDate)
const year = defaultDate.getFullYear()
const month = defaultDate.getMonth() + 1
const date = defaultDate.getDate()
const day = defaultDate.getDay()
this.calendar = {
fullDate,
year,
month,
date,
day
},
this.tempSingleDate = fullDate
this.time = getTime(defaultDate, this.hideSecond)
}
}
},
/**
* 打开日历弹窗
*/
open() {
//
if (this.clearDate && !this.insert) {
this.cale.cleanMultipleStatus()
this.init(this.date)
}
this.show = true
this.$nextTick(() => {
setTimeout(() => {
this.aniMaskShow = true
}, 50)
})
},
/**
* 关闭日历弹窗
*/
close() {
this.aniMaskShow = false
this.$nextTick(() => {
setTimeout(() => {
this.show = false
this.$emit('close')
}, 300)
})
},
/**
* 确认按钮
*/
confirm() {
this.setEmit('confirm')
this.close()
},
/**
* 变化触发
*/
change(isSingleChange) {
if (!this.insert && !isSingleChange) return
this.setEmit('change')
},
/**
* 选择月份触发
*/
monthSwitch() {
let {
year,
month
} = this.nowDate
this.$emit('monthSwitch', {
year,
month: Number(month)
})
},
/**
* 派发事件
* @param {Object} name
*/
setEmit(name) {
if (!this.range) {
if (!this.calendar.fullDate) {
this.calendar = this.cale.getInfo(new Date())
this.tempSingleDate = this.calendar.fullDate
}
if (this.hasTime && !this.time) {
this.time = getTime(new Date(), this.hideSecond)
}
}
let {
year,
month,
date,
fullDate,
extraInfo
} = this.calendar
this.$emit(name, {
range: this.cale.multipleStatus,
year,
month,
date,
time: this.time,
timeRange: this.timeRange,
fulldate: fullDate,
extraInfo: extraInfo || {}
})
},
/**
* 选择天触发
* @param {Object} weeks
*/
choiceDate(weeks) {
if (weeks.disable) return
this.calendar = weeks
this.calendar.userChecked = true
//
this.cale.setMultiple(this.calendar.fullDate, true)
this.weeks = this.cale.weeks
this.tempSingleDate = this.calendar.fullDate
const beforeDate = new Date(this.cale.multipleStatus.before).getTime()
const afterDate = new Date(this.cale.multipleStatus.after).getTime()
if (beforeDate > afterDate && afterDate) {
this.tempRange.before = this.cale.multipleStatus.after
this.tempRange.after = this.cale.multipleStatus.before
} else {
this.tempRange.before = this.cale.multipleStatus.before
this.tempRange.after = this.cale.multipleStatus.after
}
this.change(true)
},
changeMonth(type) {
let newDate
if (type === 'pre') {
newDate = this.cale.getPreMonthObj(this.nowDate.fullDate).fullDate
} else if (type === 'next') {
newDate = this.cale.getNextMonthObj(this.nowDate.fullDate).fullDate
}
this.setDate(newDate)
this.monthSwitch()
},
/**
* 设置日期
* @param {Object} date
*/
setDate(date) {
this.cale.setDate(date)
this.weeks = this.cale.weeks
this.nowDate = this.cale.getInfo(date)
}
}
}
</script>
<style lang="scss">
$uni-primary: #007aff !default;
.uni-calendar {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: column;
}
.uni-calendar__mask {
position: fixed;
bottom: 0;
top: 0;
left: 0;
right: 0;
background-color: rgba(0, 0, 0, 0.4);
transition-property: opacity;
transition-duration: 0.3s;
opacity: 0;
/* #ifndef APP-NVUE */
z-index: 99;
/* #endif */
}
.uni-calendar--mask-show {
opacity: 1
}
.uni-calendar--fixed {
position: fixed;
bottom: calc(var(--window-bottom));
left: 0;
right: 0;
transition-property: transform;
transition-duration: 0.3s;
transform: translateY(460px);
/* #ifndef APP-NVUE */
z-index: 99;
/* #endif */
}
.uni-calendar--ani-show {
transform: translateY(0);
}
.uni-calendar__content {
background-color: #fff;
}
.uni-calendar__content-mobile {
border-top-left-radius: 10px;
border-top-right-radius: 10px;
box-shadow: 0px 0px 5px 3px rgba(0, 0, 0, 0.1);
}
.uni-calendar__header {
position: relative;
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: row;
justify-content: center;
align-items: center;
height: 50px;
}
.uni-calendar__header-mobile {
padding: 10px;
padding-bottom: 0;
}
.uni-calendar--fixed-top {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: row;
justify-content: space-between;
border-top-color: rgba(0, 0, 0, 0.4);
border-top-style: solid;
border-top-width: 1px;
}
.uni-calendar--fixed-width {
width: 50px;
}
.uni-calendar__backtoday {
position: absolute;
right: 0;
top: 25rpx;
padding: 0 5px;
padding-left: 10px;
height: 25px;
line-height: 25px;
font-size: 12px;
border-top-left-radius: 25px;
border-bottom-left-radius: 25px;
color: #fff;
background-color: #f1f1f1;
}
.uni-calendar__header-text {
text-align: center;
width: 100px;
font-size: 15px;
color: #666;
}
.uni-calendar__button-text {
text-align: center;
width: 100px;
font-size: 14px;
color: $uni-primary;
/* #ifndef APP-NVUE */
letter-spacing: 3px;
/* #endif */
}
.uni-calendar__header-btn-box {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: row;
align-items: center;
justify-content: center;
width: 50px;
height: 50px;
}
.uni-calendar__header-btn {
width: 9px;
height: 9px;
border-left-color: #808080;
border-left-style: solid;
border-left-width: 1px;
border-top-color: #555555;
border-top-style: solid;
border-top-width: 1px;
}
.uni-calendar--left {
transform: rotate(-45deg);
}
.uni-calendar--right {
transform: rotate(135deg);
}
.uni-calendar__weeks {
position: relative;
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: row;
}
.uni-calendar__weeks-item {
flex: 1;
}
.uni-calendar__weeks-day {
flex: 1;
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: column;
justify-content: center;
align-items: center;
height: 40px;
border-bottom-color: #F5F5F5;
border-bottom-style: solid;
border-bottom-width: 1px;
}
.uni-calendar__weeks-day-text {
font-size: 12px;
color: #B2B2B2;
}
.uni-calendar__box {
position: relative;
// padding: 0 10px;
padding-bottom: 7px;
}
.uni-calendar__box-bg {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
justify-content: center;
align-items: center;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
.uni-calendar__box-bg-text {
font-size: 200px;
font-weight: bold;
color: #999;
opacity: 0.1;
text-align: center;
/* #ifndef APP-NVUE */
line-height: 1;
/* #endif */
}
.uni-date-changed {
padding: 0 10px;
// line-height: 50px;
text-align: center;
color: #333;
border-top-color: #DCDCDC;
;
border-top-style: solid;
border-top-width: 1px;
flex: 1;
}
.uni-date-btn--ok {
padding: 20px 15px;
}
.uni-date-changed--time-start {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
align-items: center;
}
.uni-date-changed--time-end {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
align-items: center;
}
.uni-date-changed--time-date {
color: #999;
line-height: 50px;
/* #ifdef MP-TOUTIAO */
font-size: 16px;
/* #endif */
margin-right: 5px;
// opacity: 0.6;
}
.time-picker-style {
// width: 62px;
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
justify-content: center;
align-items: center
}
.mr-10 {
margin-right: 10px;
}
.dialog-close {
position: absolute;
top: 0;
right: 0;
bottom: 0;
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: row;
align-items: center;
padding: 0 25px;
margin-top: 10px;
}
.dialog-close-plus {
width: 16px;
height: 2px;
background-color: #737987;
border-radius: 2px;
transform: rotate(45deg);
}
.dialog-close-rotate {
position: absolute;
transform: rotate(-45deg);
}
.uni-datetime-picker--btn {
border-radius: 100px;
height: 40px;
line-height: 40px;
background-color: $uni-primary;
color: #fff;
font-size: 16px;
letter-spacing: 2px;
}
/* #ifndef APP-NVUE */
.uni-datetime-picker--btn:active {
opacity: 0.7;
}
/* #endif */
</style>

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