first commit

This commit is contained in:
Ls
2026-07-15 16:21:47 +08:00
commit 43acfe2957
284 changed files with 73094 additions and 0 deletions

21
.gitignore vendored Normal file
View File

@@ -0,0 +1,21 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist
*.local
# Editor directories and files
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

23
.hbuilderx/launch.json Normal file
View File

@@ -0,0 +1,23 @@
{
// launch.json 配置了启动调试时相关设置configurations下节点名称可为 app-plus/h5/mp-weixin/mp-baidu/mp-alipay/mp-qq/mp-toutiao/mp-360/
// launchtype项可配置值为local或remote, local代表前端连本地云函数remote代表前端连云端云函数
"version" : "0.0",
"configurations" : [
{
"app-plus" : {
"launchtype" : "local"
},
"default" : {
"launchtype" : "local"
},
"mp-weixin" : {
"launchtype" : "local"
},
"type" : "uniCloud"
},
{
"playground" : "standard",
"type" : "uni-app:app-android"
}
]
}

107
AGENTS.md Normal file
View File

@@ -0,0 +1,107 @@
# AGENTS.md - Guide for Coding Agents
## Project Overview
This is a **uni-app** project (Vue 3 + TypeScript + Vite) for building multi-platform applications (primarily WeChat Mini Program, also supports H5, App, etc.). It uses uview-plus UI component library and is set up with TypeScript.
## Build & Development Commands
### Development Servers
- `npm run dev:mp-weixin` - Run dev server for WeChat Mini Program
- `npm run dev:h5` - Run dev server for H5 (browser)
- `npm run dev:app` - Run dev server for App platform
- `npm run dev:custom` - Run dev server with custom platform selection
### Production Builds
- `npm run build:mp-weixin` - Build for WeChat Mini Program
- `npm run build:h5` - Build for H5
- `npm run build:app` - Build for App platform
### Type Checking
- `npm run type-check` - Run TypeScript type checking (vue-tsc --noEmit)
## Code Style Guidelines
### Imports
- Use path alias `@/*` for imports from `src/` directory
- Example: `import { Service } from '@/Service/Service'`
- Group imports by: external libraries → internal modules
- No specific linter/formatter configured, but follow existing style
### TypeScript
- Use TypeScript for all new code
- Define types/interfaces for data models
- Use `any` sparingly, prefer proper type definitions
- Project uses `"extends": "@vue/tsconfig/tsconfig.json"`
### Naming Conventions
- **Files**: PascalCase for TypeScript classes (e.g., `Service.ts`, `HttpRequest.ts`), kebab-case for Vue components in pages (e.g., `my.vue`, `address.vue`)
- **Classes**: PascalCase (e.g., `Service`, `HttpRequest`)
- **Methods/Functions**: camelCase (e.g., `GetUserToken()`, `formatDate()`)
- **Variables**: camelCase
- **Constants**: UPPER_SNAKE_CASE (when applicable)
### Directory Structure
- `src/pages/` - Page components (main pages and subPackages)
- `src/components/` - Reusable Vue components
- `src/Service/` - API service layer and base config
- `src/common/` - Common utilities (Unit, Domain)
- `src/common/Unit/` - Helper classes (HttpRequest, StorageAssist, etc.)
- `src/common/Domain/` - Domain models (ResultData, etc.)
- `src/static/` - Static assets (images, icons, etc.)
### Error Handling
- Use `Service.Request()` for API calls which handles:
- Token authentication (adds Bearer token)
- 401 (token expired) → clears token, redirects to login
- Other error codes (40101, 1004, 40188, 1008)
- Use `uni.showToast()` for user feedback via `Service.Msg()`
- Use `Service.Alert()` for modal dialogs
### API Requests
- Always use `Service.Request(url, method, data)` for API calls
- HTTP methods: 'GET', 'POST', 'PUT'
- Request returns `Promise<ResultData>`
- `ResultData` has: `code`, `msg`, `data` properties
### UI Components
- Use **uview-plus** (imported as `u-*` components via easycom)
- Easycom auto-import configured: `^u-(.*)``uview-plus/components/u-$1/u-$1.vue`
- Color UI is also available (imported in App.vue)
### Vue Components
- Use `<script setup lang="ts">` for Vue 3 composition API
- Use uni-app lifecycle hooks: `onLaunch`, `onShow`, `onHide`, etc.
- SCSS is supported for styles
### Storage
- Use `Service.SetStorageCache(key, data)` to set storage
- Use `Service.GetStorageCache(key)` to get storage
- Use `Service.DelStorageCache(key)` to delete storage
- Token is stored with key 'token', user state with 'StateDomain'
### Navigation
- `Service.GoPage(path)` - Navigate to new page (uni.navigateTo)
- `Service.GoPageDelse(path)` - Redirect (replace current page, uni.redirectTo)
- `Service.GoPageTab(path)` - Switch to tab bar page (uni.switchTab)
- `Service.GoPageBack()` - Go back (uni.navigateBack)
### Key Utilities
- `Service.throttle(fn, time)` - Throttle function
- `Service.debounce(fn, time)` - Debounce function
- `Service.formatDate(time, type)` - Format timestamps
- `Service.Msg(message, icon?)` - Show toast
- `Service.LoadIng(text)` / `Service.LoadClose()` - Show/hide loading
## Testing
No test framework configured in this project yet.
## Existing Patterns to Follow
- Look at `src/Service/Service.ts` for how API requests are structured
- Look at `src/common/Unit/HttpRequest.ts` for lower-level request handling
- Check existing pages in `src/pages/` for component structure examples
- Use `ResultData` class from `@/common/Domain/ResultData` for API responses
## Important Notes
- This is a multi-platform project - write code compatible with uni-app standards
- Be cautious with platform-specific APIs - use uni-app conditional compilation (`#ifdef MP-WEIXIN`, etc.) if needed
- Global colors defined in App.vue: --nav-mian (#FF6A00), --nav-vice (#FF7F00), --nav-diluted (#F2C0A3)

20
index.html Normal file
View File

@@ -0,0 +1,20 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<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>
<title></title>
<!--preload-links-->
<!--app-context-->
</head>
<body>
<div id="app"><!--app-html--></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

13365
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

80
package.json Normal file
View File

@@ -0,0 +1,80 @@
{
"name": "uni-preset-vue",
"version": "0.0.0",
"scripts": {
"dev:app": "uni -p app",
"dev:app-android": "uni -p app-android",
"dev:app-ios": "uni -p app-ios",
"dev:custom": "uni -p",
"dev:h5": "uni",
"dev:h5:ssr": "uni --ssr",
"dev:mp-alipay": "uni -p mp-alipay",
"dev:mp-baidu": "uni -p mp-baidu",
"dev:mp-jd": "uni -p mp-jd",
"dev:mp-kuaishou": "uni -p mp-kuaishou",
"dev:mp-lark": "uni -p mp-lark",
"dev:mp-qq": "uni -p mp-qq",
"dev:mp-toutiao": "uni -p mp-toutiao",
"dev:mp-weixin": "uni -p mp-weixin",
"dev:mp-xhs": "uni -p mp-xhs",
"dev:quickapp-webview": "uni -p quickapp-webview",
"dev:quickapp-webview-huawei": "uni -p quickapp-webview-huawei",
"dev:quickapp-webview-union": "uni -p quickapp-webview-union",
"build:app": "uni build -p app",
"build:app-android": "uni build -p app-android",
"build:app-ios": "uni build -p app-ios",
"build:custom": "uni build -p",
"build:h5": "uni build",
"build:h5:ssr": "uni build --ssr",
"build:mp-alipay": "uni build -p mp-alipay",
"build:mp-baidu": "uni build -p mp-baidu",
"build:mp-jd": "uni build -p mp-jd",
"build:mp-kuaishou": "uni build -p mp-kuaishou",
"build:mp-lark": "uni build -p mp-lark",
"build:mp-qq": "uni build -p mp-qq",
"build:mp-toutiao": "uni build -p mp-toutiao",
"build:mp-weixin": "uni build -p mp-weixin",
"build:mp-xhs": "uni build -p mp-xhs",
"build:quickapp-webview": "uni build -p quickapp-webview",
"build:quickapp-webview-huawei": "uni build -p quickapp-webview-huawei",
"build:quickapp-webview-union": "uni build -p quickapp-webview-union",
"type-check": "vue-tsc --noEmit"
},
"dependencies": {
"@climblee/uv-ui": "^1.1.20",
"@dcloudio/uni-app": "3.0.0-4010520240507001",
"@dcloudio/uni-app-plus": "3.0.0-4010520240507001",
"@dcloudio/uni-components": "3.0.0-4010520240507001",
"@dcloudio/uni-h5": "3.0.0-4010520240507001",
"@dcloudio/uni-mp-alipay": "3.0.0-4010520240507001",
"@dcloudio/uni-mp-baidu": "3.0.0-4010520240507001",
"@dcloudio/uni-mp-jd": "3.0.0-4010520240507001",
"@dcloudio/uni-mp-kuaishou": "3.0.0-4010520240507001",
"@dcloudio/uni-mp-lark": "3.0.0-4010520240507001",
"@dcloudio/uni-mp-qq": "3.0.0-4010520240507001",
"@dcloudio/uni-mp-toutiao": "3.0.0-4010520240507001",
"@dcloudio/uni-mp-weixin": "3.0.0-4010520240507001",
"@dcloudio/uni-mp-xhs": "3.0.0-4010520240507001",
"@dcloudio/uni-quickapp-webview": "3.0.0-4010520240507001",
"clipboard": "^2.0.11",
"dayjs": "^1.11.13",
"echarts": "^5.5.1",
"uview-plus": "^3.3.54",
"vue": "^3.4.21",
"vue-i18n": "^9.1.9"
},
"devDependencies": {
"@dcloudio/types": "^3.4.8",
"@dcloudio/uni-automator": "3.0.0-4010520240507001",
"@dcloudio/uni-cli-shared": "3.0.0-4010520240507001",
"@dcloudio/uni-stacktracey": "3.0.0-4010520240507001",
"@dcloudio/vite-plugin-uni": "3.0.0-4010520240507001",
"@vue/runtime-core": "^3.4.21",
"@vue/tsconfig": "^0.1.3",
"sass": "1.63.2",
"sass-loader": "10.4.1",
"typescript": "^4.9.4",
"vite": "5.2.8",
"vue-tsc": "^1.0.24"
}
}

10
shims-uni.d.ts vendored Normal file
View File

@@ -0,0 +1,10 @@
/// <reference types='@dcloudio/types' />
import 'vue'
declare module '@vue/runtime-core' {
type Hooks = App.AppInstance & Page.PageInstance;
interface ComponentCustomOptions extends Hooks {
}
}

29
src/App.vue Normal file
View File

@@ -0,0 +1,29 @@
<script setup lang="ts">
import { onLaunch, onShow, onHide } from "@dcloudio/uni-app";
// import { NvpMerchService } from '@/Service/Nvp/NvpMerchService'
onLaunch(() => {
console.log("App Launch");
});
onShow(() => {
console.log("App Show");
});
onHide(() => {
console.log("App Hide");
});
</script>
<style lang="scss">
@import "uview-plus/index.scss";
@import "colorui/main.css";
@import "colorui/icon.css";
page {
--nav-mian: #FF6A00; //全局颜色
--nav-vice: #FF7F00; //副颜色
--nav-diluted: #F2C0A3; //淡颜色
}
</style>

13
src/Service/BaseConfig.ts Normal file
View File

@@ -0,0 +1,13 @@
export class BaseConfig {
protected static servesUrl: string = "http://192.168.0.142:5299";//线下
protected static imgUrl: string = "http://192.168.0.142:5299/";
protected static mediaUrl: string = "http://192.168.0.142:5299/";
protected static payuploadUrl: string = "http://192.168.0.142:5299";
// protected static servesUrl: string = "http://pt.api.pccsh.cn";
// protected static imgUrl: string = "http://pt.api.pccsh.cn";
// protected static mediaUrl: string = "http://pt.api.pccsh.cn";
// protected static uploadUrl: string = "/TencentCos/GetUpLoadInfo";
// protected static payuploadUrl: string = "http://pt.api.pccsh.cn";
}

344
src/Service/Service.ts Normal file
View File

@@ -0,0 +1,344 @@
import { HttpRequest, StoreAssist, UploadAssist, ResultData } from '@/common/Common';
import { BaseConfig } from './BaseConfig';
export class Service extends BaseConfig {
//获取API地址
static ApiUrl(path : string) {
return `${this.servesUrl}${path}`;
}
//获取图片地址
static GetpayImg(path : string) {
return path
if (path.startsWith('http') || path.startsWith('https')) {
return path;
} else {
return `${this.payuploadUrl}${path}`;
}
}
//获取icon图片地址
static GetIconImg(path : string) {
return path
if (path.startsWith('http') || path.startsWith('https')) {
return path;
} else {
return `${this.payuploadUrl}${path}`;
}
}
//获取图片地址
static GetMateUrlByImg(path : string) {
if (path == null || path == '') {
return '/static/none.png';
}
if (path.startsWith('http') || path.startsWith('https')) {
return path;
} else {
return `${this.imgUrl}${path}`;
}
}
//获取音视频地址
static GetMateUrlByMedia(path : string) {
if (path.startsWith('http') || path.startsWith('https')) {
return path;
} else {
return `${this.mediaUrl}${path}`;
}
}
//获取登录账号token
static GetUserToken() {
return Service.GetStorageCache('token');
}
// 获取登录状态
static GetUserIsLogin() {
var token = this.GetUserToken();
if (token == null || token == '') {
return false;
} else {
return true;
}
}
//设置登录账户Token
static SetUserToken(token : string) {
this.SetStorageCache('token', token);
}
//清理登录账户Token
static OffUserToken() {
Service.DelStorageCache('token');
uni.$emit('ImComOff', 'user');
this.ClearUserStateData();
}
//获取登录账号状态信息
static GetUserStateData() {
return Service.GetStorageCache('StateDomain');
}
//设置当前登录账号状态信息
static SetUserStateData() {
return Service.GetStorageCache('StateDomain');
}
//清理当前登录账号状态信息
static ClearUserStateData() {
Service.DelStorageCache('StateDomain');
}
//获取缓存
static GetStorageCache(key : string) {
return StoreAssist.Get(key);
}
//删除缓存
static DelStorageCache(key : string) {
StoreAssist.Delete(key);
}
//设置缓存
static SetStorageCache(key : string, data : any) {
StoreAssist.Set(key, data);
}
/*****以下是基础方法调用与拦截器*****/
static Request(url : string, method : 'GET' | 'POST' | 'PUT' | undefined, data : object | any) {
const token = Service.GetUserToken();
const _url = Service.ApiUrl(url);
var result = HttpRequest.RequestWithToken(_url, method, token, data).then((retResult : any) => {
if (retResult.statusCode == '200') {
var obj = retResult.data;
if (obj.code == 401) {
//过期
this.OffUserToken();
this.goReLaunch('/pages/login/login')
return Promise.reject();
} else if (obj.code == 40101) {
//失效
this.OffUserToken();
this.goReLaunch('/pages/login/login');
return Promise.reject();
} else if (obj.code == 1004) {
//资源不存在
this.GoPageDelse('/pages/AppSet/404/404');
return Promise.reject();
// return new ResultData(-1, '', '');
} else if (obj.code == 40188) {
//无权限
this.GoPageDelse('/pages/AppSet/40188/40188');
return Promise.reject();
// return new ResultData(-1, '', '');
} else if (obj.code == 1008) {
//业务提示
return new ResultData(obj.code, obj.msg, obj.data);
} else {
return new ResultData(obj.code, obj.msg, obj.data);
}
} else {
return new ResultData(-1, '', '');
}
});
return result;
}
/*****以下是腾讯云oss上传*****/
static UpLoadMedia(code : string, fileName : string, desire : string, path : string) {
var result = this.Request(this.uploadUrl, 'GET', { code, fileName, desire }).then((retResult) => {
if (retResult.code == 0) {
var upOk = UploadAssist.Upload(retResult.data.url, path, retResult.data.cosData).then((upRet : any) => {
if (upRet.statusCode == 200) {
const retData : any = { code: retResult.data.code, file: retResult.data.file, cache: retResult.data.cache };
return new ResultData(0, '上传成功!', retData);
} else {
this.Msg('上传失败!');
return new ResultData(-1, '', '');
}
});
return upOk;
} else {
this.Msg('上传失败!');
return new ResultData(-1, retResult.msg, retResult.data);
}
});
return result;
}
/***********消息操作**************/
static Msg(message : any, icon ?: any) : void {
if (icon != null) {
uni.showToast({
title: message,
icon: icon
});
} else {
uni.showToast({
title: message,
icon: 'none'
});
}
}
static Alert(msg : string, cb ?: any) {
uni.showModal({
title: '提示',
content: msg,
showCancel: false,
cancelText: '取消',
confirmText: '确定',
success: res => {
if (res.confirm) {
cb && cb();
}
}
})
}
static LoadIng(text : any) : void {
uni.showLoading({
title: text,
icon: 'none'
});
}
static LoadClose() : void {
uni.hideLoading();
}
/**********跳转操作*********/
// 清空页面栈
static goReLaunch(path : string) : void {
uni.reLaunch({
url: path
})
}
static GoPageTab(path : string) : void {
uni.switchTab({
url: path
});
}
/**********跳转操作*********/
static GoPage(path : string) : void {
uni.navigateTo({
url: path, //跳转的页面
success: function (res) {
// 通过eventChannel向被打开页面传送数据
}
});
}
/**********跳转并删除当前页面操作*********/
static GoPageDelse(path : string) : void {
uni.redirectTo({
url: path //跳转的页面
});
}
/**********返回上一页*********/
static GoPageBack() : void {
uni.navigateBack({ delta: 1 });
}
/*****获取图片base64*****/
static UpLoadMediaBase64(path : string) {
return new Promise(function (resolve, reject) {
uni.uploadFile({
url: 'http://cloud.pccsh.com/DefUp/UploadFileImgBase64', //仅为示例,非真实的接口地址
filePath: path,
name: 'file',
success: (uploadFileRes) => {
resolve(uploadFileRes);
},
fail: (err) => {
reject(err);
}
});
});
}
/*****获取图片位置信息*****/
//获取时间戳
static GetTimeSpan(milliSecond : number) {
return Date.now() + milliSecond;
}
// 时间戳处理
static formatDate(time : any, type : number) : string {
const date = new Date(time);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0'); // 月份从0开始所以加1并用0填充
const day = String(date.getDate()).padStart(2, '0'); // 用0填充
const hours = String(date.getHours()).padStart(2, '0'); // 用0填充
const minutes = String(date.getMinutes()).padStart(2, '0'); // 用0填充
const seconds = String(date.getSeconds()).padStart(2, '0'); // 用0填充
if (type == 0) {
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
}
else if (type == 1) {
return `${year}-${month}-${day} ${hours}:${minutes}`;
} else if (type == 2) {
return `${year}-${month}-${day}`;
} else if (type == 3) {
return `${hours}:${minutes}`;
} else if (type == 4) {
return `${year}${month}${day}`;
}
else {
return `${hours}:${minutes}`;
}
}
/*****节流*****/
static throttle(fn : () => void, time : number) {
let canRun : boolean = true;
return function () {
if (!canRun) return;
canRun = false;
setTimeout(() => {
fn(); //可以不执行
canRun = true;
}, time);
};
}
/*****防抖*****/
static debounce<T extends (...args : any[]) => void>(fn : T, time : number) : (...args : Parameters<T>) => void {
let timerId : NodeJS.Timeout | null = null;
return (...args : Parameters<T>) => {
if (timerId) {
clearTimeout(timerId);
}
timerId = setTimeout(() => {
fn(...args); // 执行传入的函数
timerId = null; // 清除定时器ID
}, time);
};
}
// 普通图片上传
static uploadH5(path, dic, callback) {
uni.uploadFile({
url: this.payuploadUrl + '/api/Upload/UploadFile',
method: "POST",
header: {
'Authorization': 'Bearer ' + Service.GetUserToken(),
},
formData: {
"path": dic,
},
filePath: path,
name: 'file',
success: (data) => {
console.log(data);
let info = data.data
callback(info)
}
})
}
}

View File

@@ -0,0 +1,42 @@
import { Service } from '@/Service/Service';
export class AddressService {
/**
* 获取用户收货地址列表
* GET /api/Address/GetUserAddressList
*/
static async GetUserAddressList(page : number) {
return await Service.Request("/api/Address/GetUserAddressList", "GET", { page });
}
/**
* 获取用户全部收货地址列表
* GET /api/Address/GetUserAllAddressList
*/
static async GetUserAllAddressList() {
return await Service.Request("/api/Address/GetUserAllAddressList", "GET", {});
}
/**
* 收货地址详情
* GET /api/Address/GetUserAddressInfo
*/
static async GetUserAddressInfo(addressId : string) {
return await Service.Request("/api/Address/GetUserAddressInfo", "GET", { addressId });
}
/**
* 添加收货地址
* POST /api/Address/AddUserAddress
*/
static async AddUserAddress(data : Record<string, any>) {
return await Service.Request("/api/Address/AddUserAddress", "POST", data);
}
/**
* 删除地址
* POST /api/Address/DelUserAddress
*/
static async DelUserAddress(data : Record<string, any>) {
return await Service.Request("/api/Address/DelUserAddress", "POST", data);
}
}

View File

@@ -0,0 +1,18 @@
import { Service } from '@/Service/Service';
export class AnnouncementService {
/**
* 获取公告列表
* GET /api/Announcement/GetAnnouncementList
*/
static async GetAnnouncementList(page: number) {
return await Service.Request("/api/Announcement/GetAnnouncementList",'GET', { page });
}
/**
* 获取公告详情
* GET /api/Announcement/GetAnnouncementInfo
*/
static async GetAnnouncementInfo(id: string) {
return await Service.Request( "/api/Announcement/GetAnnouncementInfo",'GET', { id });
}
}

View File

@@ -0,0 +1,35 @@
import { Service } from '@/Service/Service';
export class CarService {
/**
* 购物车列表
* GET /api/Car/GetUserCarList
*/
static async GetUserCarList(page: number) {
return await Service.Request( "/api/Car/GetUserCarList", 'GET',{ page });
}
/**
* 添加购物车
* POST /api/Car/AddUserCar
*/
static async AddUserCar(goodsId:string,rule:string,count:number) {
return await Service.Request( "/api/Car/AddUserCar",'POST', {goodsId,rule,count });
}
/**
* 删除购物车记录
* POST /api/Car/DelUserCar
*/
static async DelUserCar(par:string,type:number) {
return await Service.Request( "/api/Car/DelUserCar",'POST', {par,type});
}
/**
* 修改购物车数量
* POST /api/Car/UpdateCarCount
*/
static async UpdateCarCount(cgId:string,count:number) {
return await Service.Request( "/api/Car/UpdateCarCount",'POST', {cgId, count});
}
}

View File

@@ -0,0 +1,19 @@
import { Service } from '@/Service/Service';
export class CollectService {
/**
* 获取用户收藏列表
* GET /api/Collect/GetUserCollectList
*/
static async GetUserCollectList(page: number) {
return await Service.Request("/api/Collect/GetUserCollectList", 'GET',{ page });
}
/**
* 添加用户收藏列表
* POST /api/Collect/AddUserCollect
*/
static async AddUserCollect(data: Record<string, any>) {
return await Service.Request( "/api/Collect/AddUserCollect",'POST', data);
}
}

View File

@@ -0,0 +1,54 @@
import { Service } from '@/Service/Service';
export class GoodsService {
/**
* 首页接口
* GET /api/Goods/GetGoodsList
*/
static async GetGoodsList(page: number) {
return await Service.Request( "/api/Goods/GetGoodsList","GET", { page });
}
/**
* 搜索商品接口
* GET /api/Goods/GetSerchGoodsList
*/
static async GetSerchGoodsList(name: string, page: number) {
return await Service.Request("/api/Goods/GetSerchGoodsList","GET", { name, page });
}
/**
* 商品详情接口
* GET /api/Goods/GetGoodsInfo
*/
static async GetGoodsInfo(goodsId: string) {
return await Service.Request( "/api/Goods/GetGoodsInfo","GET", { goodsId });
}
/**
* 分类接口
* GET /api/Goods/GetSystemGoodsType
* @param type 普通商品/自营商品
*/
static async GetSystemGoodsType(type: string, page: number) {
return await Service.Request( "/api/Goods/GetSystemGoodsType","GET", { type, page });
}
/**
* 根据分类查询商品接口
* GET /api/Goods/GetTypeGoodsList
*/
static async GetTypeGoodsList(gtId: string, page: number) {
return await Service.Request("/api/Goods/GetTypeGoodsList", "GET",{ gtId, page });
}
/**
* 拼团商品
* GET /api/Goods/GetTuanGoodsList
*/
static async GetTuanGoodsList( page: number) {
return await Service.Request("/api/Goods/GetTuanGoodsList", "GET",{ page });
}
}

View File

@@ -0,0 +1,46 @@
import { Service } from '@/Service/Service';
export class LoginService {
/**
* 登录接口
* POST /api/Login/Login
*/
static async Login(phone :string,pwd:string ) {
return await Service.Request( "/api/Login/Login",'POST', {phone,pwd});
}
/**
* 登录接口
* POST /api/Login/PhoneLogin
*/
static async PhoneLogin(phone :string,smsCode:string ) {
return await Service.Request( "/api/Login/PhoneLogin",'POST', {phone,smsCode});
}
/**
* 注册账号
* POST /api/Login/Register
*/
static async Register(data : Record<string, any>) {
return await Service.Request( "/api/Login/Register",'POST', data);
}
/**
* 忘记密码
* POST /api/Login/ForgetPwd
*/
static async ForgetPwd(data : Record<string, any>) {
return await Service.Request( "/api/Login/ForgetPwd",'POST', data);
}
/**
* 发送验证码
* POST /api/Pub/SendSms
*/
static async SendSms(phone:string,smsCode:string) {
return await Service.Request( "/api/Pub/SendSms",'POST', { phone,smsCode });
}
}

View File

@@ -0,0 +1,75 @@
import { Service } from '@/Service/Service';
export class OrderService {
/**
* 获取微信openId
* GET /api/Order/GetOpenId
*/
static async GetOpenId(code : string) {
return await Service.Request("/api/Order/GetOpenId", 'GET', { code });
}
/**
* 下单接口
* POST /api/Order/AddUserOrder
*/
static async AddUserOrder(detail:string,isCustoms:number) {
return await Service.Request("/api/Order/AddUserOrder", 'POST', {detail,isCustoms });
}
/**
* 修改订单地址
* POST /api/Order/UpdateOrderAddress
*/
static async UpdateOrderAddress(data : Record<string, any>) {
return await Service.Request("/api/Order/UpdateOrderAddress", 'POST', data);
}
/**
* 取消订单
* POST /api/Order/CancelOrder
*/
static async CancelOrder(data : Record<string, any>) {
return await Service.Request("/api/Order/CancelOrder", 'POST', data);
}
/**
* 订单支付
* POST /api/Order/PayOrder
*/
static async PayOrder(data : Record<string, any>) {
return await Service.Request("/api/Order/PayOrder", 'POST', data);
}
/**
* 用户订单接口
* GET /api/Order/GetUserOrderList
* @param state 空全部/0待付款 1已付款 2已发货 3待评价 4已完成
*/
static async GetUserOrderList(state : string, page : number) {
return await Service.Request("/api/Order/GetUserOrderList", 'GET', { state, page });
}
/**
* 订单详情接口
* GET /api/Order/GetOrderInfo
*/
static async GetOrderInfo(orderId : string) {
return await Service.Request("/api/Order/GetOrderInfo", 'GET', { orderId });
}
/**
* 确认收货
* POST /api/Order/UserOkOrder
*/
static async UserOkOrder(data : Record<string, any>) {
return await Service.Request("/api/Order/UserOkOrder", 'POST', data);
}
/**
* 订单评价
* POST /api/Order/EvaluateOrder
*/
static async EvaluateOrder(data : Record<string, any>) {
return await Service.Request("/api/Order/EvaluateOrder", 'POST', data);
}
}

View File

@@ -0,0 +1,112 @@
import { Service } from '@/Service/Service';
export class UserService {
/**
* 获取个人信息
* GET /api/User/GetUserInfo
*/
static async GetUserInfo() {
return await Service.Request( "/api/User/GetUserInfo" ,'GET',{});
}
/**
* 修改用户信息
* POST /api/User/UpdateUser
*/
static async UpdateUser(avatar:string,nick:string) {
return await Service.Request( "/api/User/UpdateUser", 'POST', { avatar,nick });
}
/**
* 修改用户手机号
* POST /api/User/UpdateUserPhone
*/
static async UpdateUserPhone(phone:string,smsCode:string) {
return await Service.Request( "/api/User/UpdateUserPhone", 'POST', {phone,smsCode });
}
/**
* 获取用户账户信息
* GET /api/User/GetUserAccInfo
*/
static async GetUserAccInfo() {
return await Service.Request( "/api/User/GetUserAccInfo",'GET',{} );
}
/**
* 获取用户账户记录
* GET /api/User/GetUserAccLog
* @param accType 账户
* @param op 0支出1收入
*/
static async GetUserAccLog(accType: string, op: string, page: number) {
return await Service.Request("/api/User/GetUserAccLog",'GET', { accType, op, page });
}
/**
* 修改支付密码
* POST /api/User/ForgetPayPwd
*/
static async ForgetPayPwd(data: Record<string, any>) {
return await Service.Request( "/api/User/ForgetPayPwd", 'POST',data);
}
/**
* 用户评价接口
* GET /api/User/GetUserEvaluateList
*/
static async GetUserEvaluateList(source: number, page: number) {
return await Service.Request( "/api/User/GetUserEvaluateList",'GET', { source, page });
}
/**
* 删除评论接口
* POST /api/User/DelUserEvaluate
*/
static async DelUserEvaluate(data: Record<string, any>) {
return await Service.Request( "/api/User/DelUserEvaluate",'POST', data);
}
/**
* 用户提现
* POST /api/User/UserWithAcc
*/
static async UserWithAcc(data: Record<string, any>) {
return await Service.Request( "/api/User/UserWithAcc", 'POST', data);
}
/**
* 用户提现记录
* GET /api/User/GetUserWithList
* @param accType moneyAcc/shopAcc
*/
static async GetUserWithList(accType: string, page: number) {
return await Service.Request( "/api/User/GetUserWithList",'GET', { accType, page });
}
/**
* 获取推广列表
* GET /api/User/GetUserRemList
*/
static async GetUserRemList(page: number) {
return await Service.Request( "/api/User/GetUserRemList", 'GET', { page });
}
/**
* 获取推广列表
* post /api/Upload/UploadFile
*/
static async UploadFile(file: string,path:string) {
return await Service.Request( "/api/Upload/UploadFile", 'POST', { file,path });
}
/**
* 获取推广列表
* post /api/User/TranAccount
*/
static async TranAccount(amount: number,type:number) {
return await Service.Request( "/api/User/TranAccount", 'POST', { amount,type });
}
}

184
src/colorui/animation.css Normal file
View File

@@ -0,0 +1,184 @@
/*
Animation 微动画
基于ColorUI组建库的动画模块 by 文晓港 2019年3月26日19:52:28
*/
/* css 滤镜 控制黑白底色gif的 */
.gif-black{
mix-blend-mode: screen;
}
.gif-white{
mix-blend-mode: multiply;
}
/* Animation css */
[class*=animation-] {
animation-duration: .5s;
animation-timing-function: ease-out;
animation-fill-mode: both
}
.animation-fade {
animation-name: fade;
animation-duration: .8s;
animation-timing-function: linear
}
.animation-scale-up {
animation-name: scale-up
}
.animation-scale-down {
animation-name: scale-down
}
.animation-slide-top {
animation-name: slide-top
}
.animation-slide-bottom {
animation-name: slide-bottom
}
.animation-slide-left {
animation-name: slide-left
}
.animation-slide-right {
animation-name: slide-right
}
.animation-shake {
animation-name: shake
}
.animation-reverse {
animation-direction: reverse
}
@keyframes fade {
0% {
opacity: 0
}
100% {
opacity: 1
}
}
@keyframes scale-up {
0% {
opacity: 0;
transform: scale(.2)
}
100% {
opacity: 1;
transform: scale(1)
}
}
@keyframes scale-down {
0% {
opacity: 0;
transform: scale(1.8)
}
100% {
opacity: 1;
transform: scale(1)
}
}
@keyframes slide-top {
0% {
opacity: 0;
transform: translateY(-100%)
}
100% {
opacity: 1;
transform: translateY(0)
}
}
@keyframes slide-bottom {
0% {
opacity: 0;
transform: translateY(100%)
}
100% {
opacity: 1;
transform: translateY(0)
}
}
@keyframes shake {
0%,
100% {
transform: translateX(0)
}
10% {
transform: translateX(-9px)
}
20% {
transform: translateX(8px)
}
30% {
transform: translateX(-7px)
}
40% {
transform: translateX(6px)
}
50% {
transform: translateX(-5px)
}
60% {
transform: translateX(4px)
}
70% {
transform: translateX(-3px)
}
80% {
transform: translateX(2px)
}
90% {
transform: translateX(-1px)
}
}
@keyframes slide-left {
0% {
opacity: 0;
transform: translateX(-100%)
}
100% {
opacity: 1;
transform: translateX(0)
}
}
@keyframes slide-right {
0% {
opacity: 0;
transform: translateX(100%)
}
100% {
opacity: 1;
transform: translateX(0)
}
}

View File

@@ -0,0 +1,65 @@
<template>
<view>
<view class="cu-custom" :style="[{height:CustomBar + 'px'}]">
<view class="cu-bar fixed" :style="style" :class="[bgImage!=''?'none-bg text-white bg-img':'',bgColor]">
<view class="action" @tap="BackPage" v-if="isBack">
<text class="cuIcon-back"></text>
<slot name="backText"></slot>
</view>
<view class="content" :style="[{top:StatusBar + 'px'}]">
<slot name="content"></slot>
</view>
<slot name="right"></slot>
</view>
</view>
</view>
</template>
<script>
export default {
data() {
return {
StatusBar: this.StatusBar,
CustomBar: this.CustomBar
};
},
name: 'cu-custom',
computed: {
style() {
var StatusBar= this.StatusBar;
var CustomBar= this.CustomBar;
var bgImage = this.bgImage;
var style = `height:${CustomBar}px;padding-top:${StatusBar}px;`;
if (this.bgImage) {
style = `${style}background-image:url(${bgImage});`;
}
return style
}
},
props: {
bgColor: {
type: String,
default: ''
},
isBack: {
type: [Boolean, String],
default: false
},
bgImage: {
type: String,
default: ''
},
},
methods: {
BackPage() {
uni.navigateBack({
delta: 1
});
}
}
}
</script>
<style>
</style>

1226
src/colorui/icon.css Normal file

File diff suppressed because one or more lines are too long

3912
src/colorui/main.css Normal file

File diff suppressed because it is too large Load Diff

13
src/common/Common.ts Normal file
View File

@@ -0,0 +1,13 @@
import {ResultData} from "./Domain/ResultData";
import {StoreAssist} from "./Unit/StorageAssist";
import {HttpRequest} from "./Unit/HttpRequest";
import { StringAssist } from "./Unit/StringAssist";
import { UploadAssist } from "./Unit/UploadAssist";
export {
ResultData,
HttpRequest,
StoreAssist,
StringAssist,
UploadAssist
}

View File

@@ -0,0 +1,10 @@
export class ResultData {
public code: number=-1;
public msg: string="";
public data: any;
constructor(code:number,msg:string,data:any) {
this.code=code;
this.msg = msg;
this.data = data;
}
}

View File

@@ -0,0 +1,64 @@
export class HttpRequest {
/***普通请求方法***/
static Request(url: string, method: "GET" | "POST" | "PUT" | undefined, data: object | any) {
return new Promise(function(resolve, reject) {
let header = {
'content-type': method == 'POST' || method == 'PUT' ? 'application/x-www-form-urlencoded' : 'application/json; charset=utf-8',
'Access-Control-Allow-Origin': '*'
};
uni.request({
url: url,
method: method,
data: data,
header: header,
success(res: any) {
if (res.statusCode == "200") {
resolve(res.data);
}
else
{
resolve(res);
}
},
fail(err) {
//请求失败
uni.showToast({
title: '无法连接到服务器',
icon: 'none'
})
reject(err)
}
})
})
};
/***带Token的请求方法***/
static RequestWithToken(url: string, method: "GET" | "POST" | "PUT" | undefined, token: string, data: object | any) {
return new Promise(function(resolve, reject) {
let header = {
'content-type': method == 'POST' || method == 'PUT' ? 'application/x-www-form-urlencoded' : 'application/json; charset=utf-8',
'Authorization': 'Bearer ' + token,//token获取
'Access-Control-Allow-Origin': '*'
};
uni.request({
url: url,
method: method,
data: data,
header: header,
success(res: any) {
resolve(res);
},
fail(err) {
//请求失败
uni.showToast({
title: '无法连接到服务器',
icon: 'none'
})
reject(err)
}
})
})
}
}

View File

@@ -0,0 +1,14 @@
export class StoreAssist{
static Get(key:string):any
{
return uni.getStorageSync(key);
}
static Set(key:string,value:any):void
{
uni.setStorageSync(key, value);
}
static Delete(key:string):void
{
uni.removeStorageSync(key);
}
}

View File

@@ -0,0 +1,13 @@
// import { createPinia, defineStore } from "pinia";
// import piniaPluginPersistedstate from "pinia-plugin-persistedstate";
// export class StoreAssist{
// private pinia:any=createPinia();
// constructor() {
// this.pinia.use(piniaPluginPersistedstate);
// }
// }

View File

@@ -0,0 +1,98 @@
export class StringAssist {
static NoHtml(html: string): string {
return html;
}
static PhoneToStr (e:string) {
return e.substring(0,3)+'****'+e.substring((e.length-2),(e.length))
}
// 数量过万处理
static NumToStr (sum:number) {
if(sum>=10000){
return (sum/10000).toFixed('2')+'w'
}else{
return sum
}
}
// 帖子距离现在多久
static DiffTimeTostring(dateTime: string): string {
let result = 0;
let time = Date.parse(dateTime);
let timestamp = Date.parse(new Date().toString());
if ((timestamp - time) / 1000 < 60) {
result = (timestamp - time) / 1000;
result = result < 0 ? 0 : result;
return result.toFixed(0) + '秒前';
} else if ((timestamp - time) / 1000 / 60 < 60) {
return ((timestamp - time) / 1000 / 60).toFixed(0) + '分钟前';
} else if ((timestamp - time) / 1000 / 60 / 60 < 24) {
return ((timestamp - time) / 1000 / 60 / 60).toFixed(0) + '小时前';
} else if ((timestamp - time) / 1000 / 60 / 60 / 24 < 31) {
return ((timestamp - time) / 1000 / 60 / 60 / 24).toFixed(0) + '天前';
} else {
return this.formatDate(time, 1);
}
}
private static formatDate(time:any,type:number):string
{
const date = new Date(time);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0'); // 月份从0开始所以加1并用0填充
const day = String(date.getDate()).padStart(2, '0'); // 用0填充
const hours = String(date.getHours()).padStart(2, '0'); // 用0填充
const minutes = String(date.getMinutes()).padStart(2, '0'); // 用0填充
const seconds = String(date.getSeconds()).padStart(2, '0'); // 用0填充
if(type==0)
{
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
}
else if(type==1)
{
return `${year}-${month}-${day} ${hours}:${minutes}`;
}else if(type==2)
{
return `${year}-${month}-${day}`;
}else if(type==3){
return `${month}-${day} ${hours}:${minutes}`;
}
else{
return `${hours}:${minutes}`;
}
}
// 聊天时间显示
static ChatTimeTostring(dateTime: string,upTime:string): string {
let time = Date.parse(dateTime);
let timestamp = Date.parse(upTime);
if (( time - timestamp) / 1000 / 60 < 10) {
return '0';
} else{
return this.formatDate(time, 3);
}
}
// 去除两侧的空格
static trim(str:string){
const reg = /^\s+|\s+$/g;
return str.replace(reg,'');
}
// 计算两个时分秒差值
static timesfm(dateTime: string,upTime:string){
let time = Date.parse('2000-01-01 '+dateTime);
let timestamp = Date.parse('2000-01-01 '+upTime);
return ((timestamp - time) / 1000 / 60).toFixed(0) + '分钟';
}
}

View File

@@ -0,0 +1,19 @@
export class UploadAssist {
static Upload(url: string, path: string, fromData: any) {
return new Promise(function(resolve, reject) {
uni.uploadFile({
url: url, //仅为示例,非真实的接口地址
filePath: path,
name: 'file',
formData: fromData,
success: (uploadFileRes) => {
resolve(uploadFileRes);
},
fail: (err) => {
reject(err);
},
});
})
}
}

View File

@@ -0,0 +1,41 @@
<template>
<view>
<view class="" v-if="props.show">
<qf-image-cropper :src="props.url" :width="props.width" :height="props.height" :radius="0"
@crop="handleCrop"></qf-image-cropper>
</view>
</view>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue';
import { onBeforeUnmount, onMounted, reactive } from 'vue';
import { Service } from '@/Service/Service';
// const props = defineProps({
// show : string,
// // url : string,
// // width : number,
// // height:number,
// // upType:string,
// // retFun:Function
// });
let props = defineProps(["show", "url", 'width', 'height', 'upType', "retFun", "imgName"]);
onMounted(() => {
console.log(props);
})
const handleCrop = (e) => {
Service.uploadH5(e.tempFilePath, 'Avatar', rest => {
props.retFun({ url: JSON.parse(rest).data.path })
})
}
</script>
<style>
</style>

View File

@@ -0,0 +1,338 @@
<template>
<view class="container" :style="{height: height}">
<editor
class="ql-container"
:placeholder="placeholder"
:show-img-size="true"
:show-img-toolbar="true"
:show-img-resize="true"
@ready="onEditorReady"
id="editor"
@statuschange="statuschange"
@focus="editFocus"
@blur="editBlur"
ref="editot"
></editor>
<!-- 操作工具 -->
<view class="tool-view" >
<!-- 文字相关操作 -->
<view class="font-more" :style="{ height: showMoreTool ? '100rpx' : 0 }">
<jinIcon class="single" type="&#xe6e7;" font-size="44rpx" title="加粗" @click="setBold" :color="showBold ? activeColor : '#666666'"></jinIcon>
<jinIcon class="single" type="&#xe6fe;" font-size="44rpx" title="斜体" @click="setItalic" :color="showItalic ? activeColor : '#666666'"></jinIcon>
<jinIcon class="single" type="&#xe6f8;" font-size="44rpx" title="分割线" @click="setIns" :color="showIns ? activeColor : '#666666'"></jinIcon>
<jinIcon class="single" type="&#xe6e3;" font-size="44rpx" title="标题" @click="setHeader" :color="showHeader ? activeColor : '#666666'"></jinIcon>
<jinIcon class="single" type="&#xe6f1;" font-size="44rpx" title="居中" @click="setCenter" :color="showCenter ? activeColor : '#666666'"></jinIcon>
<jinIcon class="single" type="&#xe6ed;" font-size="44rpx" title="居右" @click="setRight" :color="showRight ? activeColor : '#666666'"></jinIcon>
</view>
<view class="setting-layer-mask" v-if="showSettingLayer" @click="showSetting"></view>
<view class="setting-layer" v-if="showSettingLayer">
<!-- <view class="single" @click="release(true)">
<jinIcon class="icon" type="&#xe639;" ></jinIcon>
<view>公开发布</view>
</view>
<view class="single" @click="release(false)">
<jinIcon class="icon" type="&#xe655;" ></jinIcon>
<view>暂时保存</view>
</view> -->
</view>
<view class="tool">
<jinIcon class="single" type="&#xe6f3;" font-size="44rpx" title="插入图片" @click="insertImage"></jinIcon>
<jinIcon class="single" type="&#xe6f9;" font-size="44rpx" title="修改文字样式" @click="showMore" :color="showMoreTool ? activeColor : '#666666'"></jinIcon>
<jinIcon class="single" type="&#xe6eb;" font-size="44rpx" title="分割线" @click="insertDivider"></jinIcon>
<jinIcon class="single" type="&#xe6e8;" font-size="44rpx" title="撤销" @click="undo"></jinIcon>
<jinIcon class="single" type="&#xe705;" font-size="44rpx" title="重做" @click="redo"></jinIcon>
<jinIcon class="single" type="&#xeb8a;" font-size="44rpx" title="设置" @click="showSetting"></jinIcon>
</view>
</view>
</view>
</template>
<script>
let _this
import videoUrl from '@/common/config.js'
import jinIcon from './jin-icons.vue';
export default {
props: {
// 点击图片时显示图片大小控件
showImgSize: {
type: Boolean,
default: false
},
// 点击图片时显示工具栏控件
showImgToolbar: {
type: Boolean,
default: false
},
// 点击图片时显示修改尺寸控件
showImgResize: {
type: Boolean,
default: false
},
// 占位符
placeholder: {
type: String,
default: '开始输入...'
},
// 图片上传的地址
uploadFileUrl: {
type: String,
default: '#'
},
// 上传文件时的name
fileKeyName: {
type: String,
default: 'file'
},
// 上传图片时http请求的header
header: {
type: Object
},
// 初始化html
html: {
type: String
},
// 整个控件的高度
height: {
type: String,
default: '100vh'
}
},
computed:{
},
data() {
return {
showMoreTool: false,
showBold: false,
showItalic: false,
showIns: false,
showHeader: false,
showCenter: false,
showRight: false,
showSettingLayer: false,
activeColor: '#F56C6C'
};
},
components: {
jinIcon
},
mounted() {
},
methods: {
onEditorReady(e) {
uni.createSelectorQuery()
.in(this)
.select('.ql-container')
.fields({
size: true,
context: true
},res => {
this.editorCtx = res.context;
this.editorCtx.setContents({
html: this.html
})
})
.exec();
},
undo() {
this.editorCtx.undo();
},
// 插入图片
insertImage() {
_this =this
uni.chooseImage({
count: 9, //默认9
sizeType: ['original', 'compressed'], //可以指定是原图还是压缩图,默认二者都有
sourceType: ['album', 'camera'], //从相册选择
success: async(res) => {
var tempFilePaths = res.tempFilePaths;
uni.showLoading({
title: '正在上传中...'
})
for (let temp of tempFilePaths) {
// 图片上传服务器
await uni.uploadFile({
url: this.uploadFileUrl,
filePath: temp,
name: 'file',
header: this.header,
formData: {
"path": this.fileKeyName,
},
success: res => {
// 上传完成后处理
this.editorCtx.insertImage({
src: _this.videoUrl+res.data, // 此处需要将图片地址切换成服务器返回的真实图片地址
alt: '图片',
success: function(e) {}
});
uni.hideLoading()
},
});
}
}
});
},
/// 插入分割线
insertDivider() {
this.editorCtx.insertDivider();
},
redo() {
this.editorCtx.redo();
},
showMore() {
this.showMoreTool = !this.showMoreTool;
this.editorCtx.setContents()
},
setBold() {
this.showBold = !this.showBold;
this.editorCtx.format('bold');
},
setItalic() {
this.showItalic = !this.showItalic;
this.editorCtx.format('italic');
},
checkStatus(name, detail, obj) {
if (detail.hasOwnProperty(name)) {
this[obj] = true;
} else {
this[obj] = false;
}
},
statuschange(e) {
var detail = e.detail;
this.checkStatus('bold', detail, 'showBold');
this.checkStatus('italic', detail, 'showItalic');
this.checkStatus('ins', detail, 'showIns');
this.checkStatus('header', detail, 'showHeader');
if (detail.hasOwnProperty('align')) {
if (detail.align == 'center') {
this.showCenter = true;
this.showRight = false;
} else if (detail.align == 'right') {
this.showCenter = false;
this.showRight = true;
} else {
this.showCenter = false;
this.showRight = false;
}
} else {
this.showCenter = false;
this.showRight = false;
}
},
setIns() {
this.showIns = !this.showIns;
this.editorCtx.format('ins');
},
setHeader() {
this.showHeader = !this.showHeader;
this.editorCtx.format('header', this.showHeader ? 'H2' : false);
},
setCenter() {
this.showCenter = !this.showCenter;
this.editorCtx.format('align', this.showCenter ? 'center' : false);
},
setRight() {
this.showRight = !this.showRight;
this.editorCtx.format('align', this.showRight ? 'right' : false);
},
showSetting() {
this.release(true)
// this.showSettingLayer = !this.showSettingLayer;
},
async editFocus(e) {
},
editBlur(e) {
},
release(isPublic) {
this.showSettingLayer = false;
this.editorCtx.getContents({
success: res => {
Object.assign(res, {
isPublic: isPublic
})
this.$emit('editOk', res);
}
})
},
}
};
</script>
<style scoped>
.container {
box-sizing: border-box;
display: flex;
flex-direction: column;
height: 100%;
box-sizing: border-box;
/* padding-top: 30rpx; */
}
.ql-container {
line-height: 150%;
font-size: 34rpx;
width: 100%;
background: #fff;
width: calc(100% - 60rpx);
margin: 0 auto;
flex: 1;
box-sizing: border-box;
margin-top: 30rpx;
/* padding-bottom: 5rpx; */
}
.tool-view{
width: 100vw;
background: #eee;
/* margin-top: 20px; */
}
.tool {
height: 100rpx;
display: flex;
align-items: center;
justify-content: space-around;
width: 100%;
}
.font-more {
display: flex;
align-items: center;
justify-content: space-around;
width: 100%;
background: rgb(235, 235, 235);
overflow: hidden;
transition: all 0.15s;
}
.setting-layer {
position: absolute;
bottom: 100rpx;
background: #fff;
width: 250rpx;
right: 20rpx;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
border-radius: 8rpx;
}
.setting-layer .single {
height: 80rpx;
font-size: 32rpx;
padding: 0 30rpx;
display: flex;
align-items: center;
line-height: 80rpx;
flex-direction: row;
color: #666;
}
.setting-layer .single .icon {
margin-right: 20rpx;
}
.setting-layer-mask{
position: fixed;
left: 0;
top: 0;
width: 100vw;
height: 100vh;
background: transparent;
}
</style>

View File

@@ -0,0 +1,48 @@
<template>
<view class="content">
<view class="icon" :style="{color: color, fontSize: fontSize}" v-html="type" @click="toclick"></view>
</view>
</template>
<script>
export default {
props: {
type: {
type: String,
default: '&#xe644;'
},
color: {
type: String,
default: '#666666'
},
fontSize: {
type: String,
default: '34rpx'
}
},
methods: {
toclick() {
this.$emit('click');
}
}
}
</script>
<style scoped>
.content{
display: flex;
align-items: center;
justify-content: center;
}
@font-face {
font-family: 'jin';
/** 阿里巴巴矢量图标库的字体库地址,可以替换自己的字体库地址 **/
src: url('https://at.alicdn.com/t/font_1491431_6m7ltjo8wi.ttf') format('truetype');
}
.icon {
font-family: jin !important;
font-size: 34rpx;
}
</style>

View File

@@ -0,0 +1,64 @@
## jin-edit 基于editor的富文本编辑器
### 兼容性
这是一个uni-app的通用组件兼容微信小程序端、安卓端、ios端未测试、H5端。作者因没有ios设备无法对ios端进行测试其他端测试无问题。
微信小程序 | APP | H5
:--: | :--: | :--:
√ | √ | √
我的HbuilderX版本2.6.7,不同的版本可能会造成不兼容的问题。
### 使用方式
1. 将此组件进入你的项目中的 /components/ 目录中
2. 在某个页面中使用该插件
-`script` 中引用组件
```javascript
import jinEdit from '../../components/jin-edit/jin-edit.vue';
export default {
data() {
return {
}
},
components: {
jinEdit
},
methods: {
// 点击发布
editOk(res) {
}
}
}
```
-`template` 中使用组件
```html
<jinEdit placeholder="请输入内容" @editOk="editOk" uploadFileUrl="/#"></jinEdit>
```
### Demo
[uni-jin一个uni-app组件集合](https://github.com/wangjinxin613/uni-jin)
### 参数
属性 | 类型 | 默认值 | 说明
:--: | :--: | :--: | :--:
showImgSize | Boolean | false | 点击图片时显示图片大小控件
showImgToolbar | Boolean | false | 点击图片时显示工具栏控件
showImgResize | Boolean | false | 点击图片时显示修改尺寸控件
placeholder | String | '' | 编辑器占位符
uploadFileUrl | String | '#' | 图片上传的服务器地址
fileKeyName | String | 'file' | 图片上传时的name
header | Object | - | 图片上传http请求的header
html | String | - | 初始化的html
### 方法
方法名 | 参数 | 说明
:--: | :--: | :--:
editOk | e={html,text,delta,isPublic} | 点击发布按钮触发
以上

View File

@@ -0,0 +1,299 @@
<template>
<view class="content">
<view class="liy-select-fixed" :class="openClass">
<view class="liy-search-warp">
<view class="liy-search-input">
<view class="lsi-warp">
<image src="../../static/liy-select/images/search.png" mode="widthFix"
class="lsi-icon"></image>
<input class="lsi-input" v-model="keyword" placeholder="请输入搜索内容" @input="getParamsList" />
</view>
</view>
<scroll-view scroll-y="true" class="scroll-Y" @scrolltolower="lower">
<view class="liy-search-list" v-if="outPutList.length > 0">
<view class="liy-search-li" v-for="(item,index) in outPutList" :key="index"
@click="mapSelectMenu(item,index)">
<view class="liy-search-left">
<view class="liy-search-title" v-if="titleKey">{{item[titleKey]}}
</view>
<view class="liy-search-title" v-else>未选择标题
</view>
<view class="liy-search-desc" v-if="subtitleKey">{{item[subtitleKey]}}</view>
</view>
<view class="liy-search-icon" v-if="mapSelectIndex == index">
<image class="lsi-icon" src="../../static/liy-select/images/check_mark.png"
mode="widthFix"></image>
</view>
</view>
<view class="liy-loading">
<image class="liy-loading-img" src="../../static/liy-select/images/complete.png"
mode="widthFix"></image>
<view class="liy-loading-text">加载完成</view>
</view>
</view>
<view v-else class="liy-void">
<image class="liy-void-img" src="../../static/liy-select/images/void.png"
mode="widthFix"></image>
<view class="liy-void-text">未查询到内容</view>
</view>
</scroll-view>
</view>
</view>
<view class="liy-overlay" @click="closeOverlay" v-if="open == true"></view>
</view>
</template>
<script>
export default {
name:'liy-select',
props:{
list:{
type:Array,
default:[]
},
titleKey:{
type:[String,null],
default: null
},
subtitleKey:{
type:[String,null],
default:null
},
open:{
type:Boolean,
default:false
}
},
data() {
return {
mapSelectIndex: null,
keyword: "",
outPutList: [],
openClass:""
}
},
watch: {
list (newList, oldList) {
this.getParamsList();
},
open(newOpen,oldOpen){
if(newOpen == true){
this.openClass = "show";
}else{
this.openClass = "hide";
}
}
},
created() {
this.getParamsList();
},
methods: {
mapSelectMenu(item,index) {
this.mapSelectIndex = index;
this.$emit("change",item,index)
this.$emit("close");
},
getParamsList() {
var selectList = this.list;
this.mapSelectIndex = null;
if (!this.keyword) {
this.outPutList = selectList;
return false;
}
var arr = [];
for (var i = 0; i < selectList.length; i++) {
let item = selectList[i];
if (item[this.titleKey].indexOf(this.keyword) > -1) {
arr.push(item);
} else {
}
}
this.outPutList = arr;
},
lower() {
},
closeOverlay() {
this.$emit("close");
}
}
}
</script>
<style scoped lang="scss">
.liy-overlay {
transition-duration: 300ms;
transition-timing-function: ease-out;
position: fixed;
inset: 0px;
z-index: 100;
background-color: rgba(0, 0, 0, 0.5);
}
.liy-select-fixed {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
z-index: 150;
transition: transform .5s ease-in-out;
transform: translateY(calc(100% + 40rpx));
&.show {
animation: showHandler 0.3s forwards;
}
&.hide {
animation: hideHandler 0.3s forwards;
}
.liy-search-warp {
background: #fff;
border-radius: 10rpx 10rpx 0 0;
.liy-search-input {
padding: 30rpx 40rpx;
font-size: 28rpx;
.lsi-warp {
display: flex;
align-items: center;
border: 2rpx solid #e3e3e3;
padding: 16rpx 20rpx;
border-radius: 50rpx;
.lsi-icon {
width: 30rpx;
height: 30rpx;
}
.lsi-input {
width: 90%;
padding-left: 16rpx;
font-size: 28rpx;
}
}
}
.liy-search-list {
.liy-search-li {
padding: 20rpx 30rpx;
display: flex;
align-items: center;
border-bottom: 1rpx solid #f7f7f7;
min-height: 50rpx;
.liy-search-left {
width: 90%;
.liy-search-title {
font-size: 28rpx;
line-height: 36rpx;
color: #333;
}
.liy-search-desc {
padding-top: 10rpx;
font-size: 24rpx;
line-height: 36rpx;
color: #999;
}
}
.liy-search-icon {
.lsi-icon {
width: 40rpx;
height: 40rpx;
}
}
}
}
}
}
.scroll-Y {
height: 500rpx;
}
.liy-loading {
color: #333;
font-size: 26rpx;
display: flex;
align-items: center;
justify-content: center;
padding: 20rpx 0;
.liy-loading-img {
width: 32rpx;
height: 32rpx;
// animation: turn 3s linear infinite;
}
.liy-loading-text {
padding-left: 12rpx;
padding-right: 32rpx;
}
}
@keyframes turn {
0% {
transform: rotate(0deg);
}
25% {
transform: rotate(90deg);
}
50% {
transform: rotate(180deg);
}
75% {
transform: rotate(270deg);
}
100% {
transform: rotate(360deg);
}
}
@keyframes showHandler {
0% {
transform: translateY(calc(100% + 40rpx));
}
100% {
transform: translateY(0);
}
}
@keyframes hideHandler {
0% {
transform: translateY(0);
}
100% {
transform: translateY(calc(100% + 40rpx));
}
}
.liy-void {
text-align: center;
padding-top: 60rpx;
.liy-void-img {
width: 40%;
}
.liy-void-text {
padding-top: 30rpx;
font-size: 26rpx;
color: #909090;
}
}
</style>

8
src/env.d.ts vendored Normal file
View File

@@ -0,0 +1,8 @@
/// <reference types="vite/client" />
declare module '*.vue' {
import { DefineComponent } from 'vue'
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types
const component: DefineComponent<{}, {}, any>
export default component
}

13
src/main.ts Normal file
View File

@@ -0,0 +1,13 @@
import { createSSRApp } from "vue";
import App from "./App.vue";
import uviewPlus from 'uview-plus'
export function createApp() {
const app = createSSRApp(App);
app.use(uviewPlus)
return {
app,
};
}

145
src/manifest.json Normal file
View File

@@ -0,0 +1,145 @@
{
"name" : "",
"appid" : "__UNI__06C2D6A",
"description" : "",
"versionName" : "1.0.8",
"versionCode" : 108,
"transformPx" : false,
/* 5+App */
"app-plus" : {
"compatible" : {
"ignoreVersion" : true //true表示忽略版本检查提示框HBuilderX1.9.0及以上版本支持
},
"usingComponents" : true,
"nvueStyleCompiler" : "uni-app",
"compilerVersion" : 3,
"splashscreen" : {
"alwaysShowBeforeRender" : false,
"waiting" : false,
"autoclose" : true,
"delay" : 0
},
/* */
"modules" : {
"Barcode" : {},
"Maps" : {},
"Geolocation" : {},
"Share" : {}
},
/* */
"distribute" : {
/* android */
"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\"/>"
],
"minSdkVersion" : 25,
"targetSdkVersion" : 25,
"abiFilters" : [ "armeabi-v7a", "arm64-v8a", "x86" ]
},
/* ios */
"ios" : {
"idfa" : false,
"dSYMs" : false
},
/* SDK */
"sdkConfigs" : {
"ad" : {},
"maps" : {
"amap" : {
"name" : "amapZAvZjTHj",
"appkey_ios" : "3caf9e6f01b0085be1e75e0d0e281fe7",
"appkey_android" : "3caf9e6f01b0085be1e75e0d0e281fe7"
}
},
"geolocation" : {
"amap" : {
"name" : "amapZAvZjTHj",
"__platform__" : [ "android" ],
"appkey_ios" : "",
"appkey_android" : "3caf9e6f01b0085be1e75e0d0e281fe7"
}
},
"share" : {}
},
"icons" : {
"android" : {
"hdpi" : "unpackage/res/icons/72x72.png",
"xhdpi" : "unpackage/res/icons/96x96.png",
"xxhdpi" : "unpackage/res/icons/144x144.png",
"xxxhdpi" : "unpackage/res/icons/192x192.png"
},
"ios" : {
"appstore" : "unpackage/res/icons/1024x1024.png",
"ipad" : {
"app" : "unpackage/res/icons/76x76.png",
"app@2x" : "unpackage/res/icons/152x152.png",
"notification" : "unpackage/res/icons/20x20.png",
"notification@2x" : "unpackage/res/icons/40x40.png",
"proapp@2x" : "unpackage/res/icons/167x167.png",
"settings" : "unpackage/res/icons/29x29.png",
"settings@2x" : "unpackage/res/icons/58x58.png",
"spotlight" : "unpackage/res/icons/40x40.png",
"spotlight@2x" : "unpackage/res/icons/80x80.png"
},
"iphone" : {
"app@2x" : "unpackage/res/icons/120x120.png",
"app@3x" : "unpackage/res/icons/180x180.png",
"notification@2x" : "unpackage/res/icons/40x40.png",
"notification@3x" : "unpackage/res/icons/60x60.png",
"settings@2x" : "unpackage/res/icons/58x58.png",
"settings@3x" : "unpackage/res/icons/87x87.png",
"spotlight@2x" : "unpackage/res/icons/80x80.png",
"spotlight@3x" : "unpackage/res/icons/120x120.png"
}
}
}
}
},
/* */
"quickapp" : {},
/* */
"mp-weixin" : {
"appid" : "",
"setting" : {
"urlCheck" : false
},
"usingComponents" : true
},
"mp-alipay" : {
"usingComponents" : true
},
"mp-baidu" : {
"usingComponents" : true
},
"mp-toutiao" : {
"usingComponents" : true
},
"uniStatistics" : {
"enable" : false
},
"vueVersion" : "3",
"h5" : {
"sdkConfigs" : {
"maps" : {
"qqmap" : {
"key" : "7DIBZ-K4HCJ-ZR2FE-FOOOP-SALFT-RLFYW"
}
}
}
}
}

16
src/package.json Normal file
View File

@@ -0,0 +1,16 @@
{
"id": "liy-select",
"name": "可搜索的下拉选择框",
"displayName": "可搜索的下拉选择框",
"version": "1.0.0",
"description": "通过搜索关键字可以进行 下拉筛选",
"keywords": [
"uni-ui|select"
],
"dcloudext": {
"category": [
"前端组件",
"通用组件"
]
}
}

270
src/pages.json Normal file
View File

@@ -0,0 +1,270 @@
{
"easycom": {
// 注意一定要放在custom里否则无效https://ask.dcloud.net.cn/question/131175
"custom": {
"^u--(.*)": "uview-plus/components/u-$1/u-$1.vue",
"^up-(.*)": "uview-plus/components/u-$1/u-$1.vue",
"^u-([^-].*)": "uview-plus/components/u-$1/u-$1.vue"
}
},
"pages": [ //pages数组中第一项表示应用启动页参考https://uniapp.dcloud.io/collocation/pages
{
"path": "pages/index/index",
"style": {
"navigationBarTitleText": "首页",
"navigationBarBackgroundColor": "#36394D",
"navigationStyle": "custom",
"backgroundColor": "#F8F8F8"
}
},
{
"path": "pages/login/login",
"style": {
"navigationBarTitleText": "登录",
"navigationBarBackgroundColor": "#FFFFFF",
"navigationStyle": "custom",
"backgroundColor": "#FFF7ED"
}
},
{
"path": "pages/login/register",
"style": {
"navigationBarTitleText": "注册",
"navigationBarBackgroundColor": "#FFFFFF",
"backgroundColor": "#FFF7ED"
}
},
{
"path": "pages/login/forgotPassword",
"style": {
"navigationBarTitleText": "忘记密码",
"navigationBarBackgroundColor": "#FFFFFF",
"backgroundColor": "#FFF7ED"
}
},
{
"path": "pages/index/class",
"style": {
"navigationBarTitleText": "分类",
"backgroundColor": "#F8F8F8"
}
},
{
"path": "pages/index/shop",
"style": {
"navigationBarTitleText": "购物车"
}
},
{
"path": "pages/index/my",
"style": {
"navigationBarTitleText": "我的",
"navigationStyle": "custom"
}
},
{
"path": "pages/login/changgePassword",
"style": {
"navigationBarTitleText": "修改密码"
}
},
{
"path": "pages/login/changePhone",
"style": {
"navigationBarTitleText": "修改手机号"
}
},
{
"path": "pages/login/changePay",
"style": {
"navigationBarTitleText": "支付密码"
}
}
],
"globalStyle": {
"navigationBarTextStyle": "black",
"navigationBarTitleText": "v派商家",
"navigationBarBackgroundColor": "#ffffff",
"backgroundColor": "#ffffff"
},
"subPackages": [{
"root": "pages/userFunc",
"pages": [{
"path": "webView",
"style": {
"navigationBarTitleText": "功能页"
}
},
{
"path": "notice",
"style": {
"navigationBarTitleText": "公告"
}
},
{
"path": "set",
"style": {
"navigationBarTitleText": "设置"
}
},
{
"path": "addressList",
"style": {
"navigationBarTitleText": "我的地址"
}
},
{
"path": "addAddress",
"style": {
"navigationBarTitleText": "添加地址"
}
},
{
"path": "noticeList",
"style": {
"navigationBarTitleText": "公告列表"
}
},
{
"path": "goodsSearch",
"style": {
"navigationBarTitleText": "搜索"
}
},
{
"path": "searchList",
"style": {
"navigationBarTitleText": "搜索内容"
}
},
{
"path": "collect",
"style": {
"navigationBarTitleText": "我的收藏"
}
},
{
"path": "promote",
"style": {
"navigationBarTitleText": "推广"
}
},
{
"path": "comment",
"style": {
"navigationBarTitleText": "我的评价"
}
},
{
"path": "goComment",
"style": {
"navigationBarTitleText": "评价"
}
},
{
"path": "setuserData",
"style": {
"navigationBarTitleText": "设置"
}
},
{
"path": "walletRecord",
"style": {
"navigationBarTitleText": "收支明细"
}
},
{
"path": "withDrow",
"style": {
"navigationBarTitleText": "提现"
}
},
{
"path": "transfer",
"style": {
"navigationBarTitleText": "转账"
}
},
{
"path": "withdrowList",
"style": {
"navigationBarTitleText": "提现记录"
}
},
{
"path": "wallet",
"style": {
"navigationBarTitleText": "钱包"
}
},
{
"path": "Groupsharing",
"style": {
"navigationBarTitleText": "拼单商品"
}
}
]
},
{
"root": "pages/webView",
"pages": [{
"path": "goodsDetail",
"style": {
"navigationBarTitleText": "商品详情",
"navigationStyle": "custom"
}
},
{
"path": "orderList",
"style": {
"navigationBarTitleText": "订单",
"navigationStyle": "custom"
}
},
{
"path": "orderDetail",
"style": {
"navigationBarTitleText": "订单详情",
"navigationStyle": "custom"
}
}
]
}
],
"tabBar": {
"color": "#6B7280",
"selectedColor": "#F97316",
"backgroundColor": "#FFFFFF",
"height": "50px",
"list": [{
"pagePath": "pages/index/index",
"iconPath": "static/tab/01.png",
"selectedIconPath": "static/tab/02.png",
"text": "主页"
},
{
"pagePath": "pages/index/class",
"iconPath": "static/tab/class.png",
"selectedIconPath": "static/tab/classed.png",
"text": "分类"
},
{
"pagePath": "pages/index/shop",
"iconPath": "static/tab/shop.png",
"selectedIconPath": "static/tab/shoped.png",
"text": "购物车"
},
{
"pagePath": "pages/index/my",
"iconPath": "static/tab/user.png",
"selectedIconPath": "static/tab/usered.png",
"text": "我的"
}
]
}
}

540
src/pages/index/class.vue Normal file
View File

@@ -0,0 +1,540 @@
<template>
<view>
<!-- 切换栏 -->
<view class="tab-bar">
<view class="tab-item" :class="{ active: activeTab === 0 }" @click="switchTab(0)">
<text>普通商品</text>
</view>
<view class="tab-item" :class="{ active: activeTab === 1 }" @click="switchTab(1)">
<text>自营商品</text>
</view>
</view>
<!-- 骨架屏区域 - 数据加载时显示 -->
<view v-if="isLoading" class="skeleton-container" style="padding-top: 80rpx; height: 100vh;">
<!-- 左侧分类栏骨架屏 -->
<view class="skeleton-left-sidebar">
<view v-for="i in 8" :key="i" class="skeleton-sidebar-item">
<view class="skeleton-item skeleton-shimmer"></view>
</view>
</view>
<!-- 右侧内容区域骨架屏 -->
<view class="skeleton-right-content">
<!-- 商品列表骨架 -->
<view class="skeleton-goods-list">
<view v-for="i in 4" :key="i" class="skeleton-goods-item">
<view class="skeleton-goods-img skeleton-item skeleton-shimmer"></view>
<view class="skeleton-goods-info">
<view class="skeleton-goods-name skeleton-item skeleton-shimmer"></view>
<view class="skeleton-goods-name skeleton-item skeleton-shimmer short"></view>
<view class="skeleton-goods-sales skeleton-item skeleton-shimmer"></view>
<view class="skeleton-goods-bottom">
<view class="skeleton-goods-price skeleton-item skeleton-shimmer"></view>
</view>
</view>
</view>
</view>
</view>
</view>
<!-- 实际内容区域 - 数据加载完成后显示 -->
<view v-else class="category-container" style="padding-top: 80rpx; height: 100vh;">
<!-- 左侧分类栏 -->
<scroll-view scroll-y class="left-sidebar" :style="{ height: `calc(100vh - 80rpx)` }">
<view @click="changeClass(classIndex)" v-for="(classItem, classIndex) in classList" :key="classIndex"
:class="{ active: classIndex === nowClass, noactive: classIndex !== nowClass }"
class="sidebar-item">
{{ classItem?.gtName }}
</view>
</scroll-view>
<!-- 右侧内容区域 -->
<scroll-view scroll-y class="right-content" :style="{ height: `calc(100vh - 80rpx)` }"
@scrolltolower="loadMoreGoods" lower-threshold="100">
<view class="goods-list">
<view class="goods-item" @click="Service.GoPage('/pages/webView/goodsDetail?goodsId='+item.goodsId)"
v-for="(item, index) in goodsList" :key="index">
<img :src="Service.GetMateUrlByImg(item.goodsImg)" class="goods-img" alt="" />
<view class="goods-info">
<view class="goods-name">{{ item.goodsName }}</view>
<view class="goods-sales">已售{{ item.sale }}+</view>
<view class="goods-bottom">
<view class="goods-price">
<text class="new-price">{{ item.price }}</text>
<text class="old-price">{{ item.maxPrice }}</text>
</view>
</view>
</view>
</view>
</view>
<up-loadmore :status="status" />
</scroll-view>
</view>
</view>
</template>
<script setup lang="ts">
import { onShow, onLoad, onReachBottom } from "@dcloudio/uni-app";
import { Service } from '@/Service/Service'
import { ref } from "vue";
import { GoodsService } from '@/Service/api/GoodsService'
// ==================== 搜索和分类相关变量 ====================
let searchWord = ref() // 搜索关键词
let nowClass = ref<number>(0) // 当前选中的分类索引
let loading = ref(false) // 加载状态
let noMore = ref(false) // 是否没有更多数据
let activeTab = ref<number>(0)
// ==================== 分页相关变量 ====================
let status = ref('nomore') // 加载状态loadmore/loading/nomore
let page = ref(1) // 当前页码
// ==================== 数据列表变量 ====================
let classList = ref<Array<any>>([]) // 分类列表
let goodsList = ref<Array<any>>([]) // 商品列表
// ==================== 骨架屏相关变量 ====================
let isLoading = ref(true) // 控制骨架屏显示/隐藏的状态
// ==================== 生命周期钩子 ====================
onLoad(() => {
// 页面加载时获取分类数据
GetSystemGoodsType()
});
onShow(() => {
// 页面显示时的逻辑
});
onReachBottom(() => {
// 滚动到底部时加载更多商品
getList()
})
// ==================== 数据加载方法 ====================
/**
* 初始化商品数据
* 清空现有商品列表,重置页码,开始加载第一页
*/
const getData = () => {
goodsList.value = []
status.value = 'loadmore'
page.value = 1
getList()
}
/**
* 加载商品列表数据
* 根据当前选中的分类获取商品
*/
const getList = () => {
// 防止重复加载
if (status.value == 'loading' || status.value == 'nomore') {
return
}
status.value = 'loading'
// 调用API获取分类商品
GoodsService.GetTypeGoodsList(classList.value[nowClass.value].gtId, page.value).then(res => {
if (res.code == 0) {
goodsList.value = [...goodsList.value, ...res.data.goodsList]
status.value = res.data.goodsList.length == 10 ? 'loadmore' : 'nomore'
page.value++
} else {
Service.Msg(res.msg)
}
})
}
/**
* 获取系统商品分类
* 同时加载第一页的商品数据
*/
const GetSystemGoodsType = () => {
GoodsService.GetSystemGoodsType( activeTab.value==0?'普通商品':'自营商品', page.value).then(res => {
if (res.code == 0) {
classList.value = res.data.goodsType
goodsList.value = [...goodsList.value, ...res.data.goodsList]
status.value = res.data.goodsList.length == 10 ? 'loadmore' : 'nomore'
page.value++
// 数据加载完成,隐藏骨架屏
isLoading.value = false
} else {
Service.Msg(res.msg)
// 出错时也隐藏骨架屏
isLoading.value = false
}
})
}
/**
* 切换分类
* @param val 分类索引
*/
const changeClass = (val : number) => {
nowClass.value = val
console.log(nowClass.value);
getData()
}
/**
* 切换 Tab保单商品/自营商品)
*/
const switchTab = (val: number) => {
if (activeTab.value === val) return
activeTab.value = val
nowClass.value = 0
goodsList.value=[]
page.value=1
GetSystemGoodsType()
}
/**
* 滚动到底部加载更多商品
*/
const loadMoreGoods = () => {
getList()
}
</script>
<style scoped lang="scss">
// ==================== 基础样式 ====================
page {
background-color: #fff;
}
/* ===== 切换栏 ===== */
.tab-bar {
position: fixed;
top: 0rpx;
left: 0;
z-index: 99;
display: flex;
width: 100%;
background: #fff;
border-bottom: 1rpx solid #f0f0f0;
}
.tab-item {
flex: 1;
padding: 24rpx 0;
text-align: center;
font-size: 28rpx;
color: #666;
position: relative;
transition: color 0.2s;
}
.tab-item.active {
color: var(--nav-mian);
font-weight: 600;
}
.tab-item.active::after {
content: '';
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 60rpx;
height: 4rpx;
background: var(--nav-mian);
border-radius: 2rpx;
}
.category-container {
display: flex;
width: 100%;
overflow: hidden;
}
.left-sidebar {
width: 200rpx;
background-color: #F5F5F5;
flex-shrink: 0;
}
.sidebar-item {
padding: 30rpx 20rpx;
text-align: center;
font-size: 28rpx;
transition: all 0.3s;
}
.right-content {
flex: 1;
background-color: #fff;
padding: 20rpx;
}
.banner {
width: 100%;
height: 200rpx;
margin-bottom: 20rpx;
}
.goods-list {
display: flex;
flex-direction: column;
}
.goods-item {
width: 100%;
margin-bottom: 20rpx;
background-color: #fff;
border-radius: 16rpx;
overflow: hidden;
display: flex;
padding: 20rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
}
.goods-img {
width: 200rpx;
height: 200rpx;
border-radius: 12rpx;
flex-shrink: 0;
}
.goods-info {
flex: 1;
margin-left: 20rpx;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.goods-name {
font-size: 28rpx;
font-weight: 500;
color: #333;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
line-height: 1.5;
}
.goods-bottom {
display: flex;
justify-content: space-between;
align-items: flex-end;
}
.goods-price {
display: flex;
align-items: baseline;
}
.new-price {
font-weight: 700;
color: var(--nav-mian);
font-size: 36rpx;
}
.old-price {
margin-left: 10rpx;
font-size: 22rpx;
text-decoration: line-through;
color: #999;
}
.goods-sales {
font-size: 22rpx;
color: #999;
}
.loading-text {
text-align: center;
padding: 30rpx 0;
color: #999;
font-size: 26rpx;
}
.active {
background-color: #fff;
color: var(--nav-mian);
font-weight: 600;
position: relative;
}
.noactive {
background-color: transparent;
color: #333;
}
.border-20 {
border-radius: 20rpx;
}
.padding-20 {
padding: 20rpx;
}
// ==================== 骨架屏样式 ====================
// 骨架屏容器 - 左右布局
.skeleton-container {
display: flex;
width: 100%;
overflow: hidden;
}
// 骨架屏基础元素样式 - 渐变灰色占位块
.skeleton-item {
// 使用柔和的渐变背景,从浅灰到稍深的灰色
background: linear-gradient(135deg,
#f8f8f8 0%,
#f0f0f0 50%,
#e8e8e8 100%);
border-radius: 8rpx;
}
// ==================== 闪光动画效果 ====================
// 使用CSS动画实现骨架屏的闪光效果
.skeleton-shimmer {
position: relative;
overflow: hidden;
// 创建闪光层
&::after {
content: '';
position: absolute;
top: 0;
left: -100%; // 从左侧开始
width: 100%;
height: 100%;
// 使用多层渐变创造更丰富的闪光效果
background: linear-gradient(90deg,
transparent 0%,
rgba(255, 255, 255, 0.3) 20%, // 淡入
rgba(255, 255, 255, 0.6) 40%, // 最亮
rgba(255, 255, 255, 0.4) 60%, // 稍暗
rgba(255, 255, 255, 0.2) 80%, // 淡出
transparent 100%);
animation: skeleton-shimmer 2s infinite; // 循环播放动画,稍慢一点更自然
}
// 添加一个额外的渐变层,创造更深的视觉层次
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(180deg,
rgba(255, 255, 255, 0.1) 0%,
transparent 50%,
rgba(0, 0, 0, 0.03) 100%);
pointer-events: none; // 不影响点击
}
}
// 闪光动画关键帧
@keyframes skeleton-shimmer {
0% {
transform: translateX(-100%); // 起始位置
}
100% {
transform: translateX(100%); // 结束位置
}
}
// ==================== 左侧分类栏骨架屏 ====================
.skeleton-left-sidebar {
width: 200rpx;
background-color: #F5F5F5;
flex-shrink: 0;
padding: 20rpx 0;
}
.skeleton-sidebar-item {
padding: 30rpx 20rpx;
display: flex;
justify-content: center;
}
.skeleton-sidebar-item .skeleton-item {
width: 120rpx;
height: 28rpx;
}
// ==================== 右侧内容区域骨架屏 ====================
.skeleton-right-content {
flex: 1;
background-color: #fff;
padding: 20rpx;
}
// 商品列表骨架
.skeleton-goods-list {
display: flex;
flex-direction: column;
gap: 20rpx;
}
.skeleton-goods-item {
width: 100%;
background-color: #fff;
border-radius: 16rpx;
overflow: hidden;
display: flex;
padding: 20rpx;
}
.skeleton-goods-img {
width: 200rpx;
height: 200rpx;
border-radius: 12rpx;
flex-shrink: 0;
}
.skeleton-goods-info {
flex: 1;
margin-left: 20rpx;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.skeleton-goods-name {
width: 100%;
height: 28rpx;
margin-bottom: 12rpx;
&.short {
width: 60%;
margin-bottom: 8rpx;
}
}
.skeleton-goods-sales {
width: 100rpx;
height: 24rpx;
margin-bottom: 16rpx;
}
.skeleton-goods-bottom {
display: flex;
justify-content: space-between;
align-items: flex-end;
}
.skeleton-goods-price {
width: 140rpx;
height: 36rpx;
}
</style>

832
src/pages/index/index.vue Normal file
View File

@@ -0,0 +1,832 @@
<template>
<view class="home-page">
<!-- 搜索栏 -->
<view class="search-bar">
<view class="search-box" @click="Service.GoPage('/pages/userFunc/goodsSearch')">
<u-icon name="search" color="#999" size="18"></u-icon>
<view class="search-center">
<text class="search-placeholder">搜索宝贝</text>
</view>
</view>
</view>
<!-- 主内容 -->
<view class="main-content">
<!-- 轮播图 -->
<view class="swiper-wrap">
<up-swiper :list="swiperList" height="200" indicator indicatorMode="dot" circular radius="16"
bgColor="#ffffff"></up-swiper>
</view>
<!-- 公告栏 -->
<view class="notice-bar">
<up-notice-bar :text="gonggao.fubiaoti " bgColor='#fff' ></up-notice-bar>
<text class="notice-more" @click="Service.GoPage('/pages/userFunc/noticeList')">更多</text>
</view>
<!-- 爆单商品横向滚动 -->
<view v-if="scrollList.length>0" class="section-card preferred-section">
<view class="section-header">
<view class="header-left">
<view class="section-icon">
<u-icon name="heart-fill" color="#fff" size="16"></u-icon>
</view>
<text class="section-title">报单商品</text>
</view>
</view>
<scroll-view scroll-x class="preferred-scroll" show-scrollbar="false">
<view class="preferred-item" v-for="(item, index) in scrollList" :key="index"
@click="goGoodsDetail(item.goodsId)">
<view class="preferred-img-wrap">
<image class="preferred-img" :src="item.goodsImg" mode="aspectFill"></image>
</view>
<view class="preferred-name">{{ item.goodsName }}</view>
<view class="preferred-price-row">
<text class="preferred-price">¥{{ item.price }}</text>
<text class="preferred-old">¥{{ item.maxPrice }}</text>
</view>
</view>
</scroll-view>
</view>
<!-- 商品瀑布流 -->
<view class="goods-waterfall">
<view class="waterfall-col">
<view class="goods-card" v-for="(item, index) in leftList" :key="index"
@click="goGoodsDetail(item.goodsId)">
<view class="goods-img-wrap">
<image class="goods-img" :src="item.goodsImg" mode="widthFix"></image>
<view class="goods-label">{{ item.goodsType }}</view>
</view>
<view class="goods-info">
<view class="goods-name">{{ item.goodsName }}</view>
<view class="goods-price-row">
<view class="price-wrap">
<text class="goods-price">¥<text class="price-num">{{ item.price }}</text></text>
<text class="goods-old">¥{{ item.maxPrice }}</text>
</view>
<text class="goods-sold">{{ item.sale }}人付款</text>
</view>
</view>
</view>
</view>
<view class="waterfall-col">
<view class="goods-card" v-for="(item, index) in rightList" :key="index"
@click="goGoodsDetail(item.goodsId)">
<view class="goods-img-wrap">
<image class="goods-img" :src="item.goodsImg" mode="widthFix"></image>
<view class="goods-label">{{ item.goodsType }}</view>
</view>
<view class="goods-info">
<view class="goods-name">{{ item.goodsName }}</view>
<view class="goods-tags" v-if="item.tags && item.tags.length">
<text class="tag-text" v-for="(tag, tIndex) in item.tags" :key="tIndex">{{ tag }}</text>
</view>
<view class="goods-price-row">
<view class="price-wrap">
<text class="goods-price">¥<text class="price-num">{{ item.price }}</text></text>
<text class="goods-old">¥{{ item.maxPrice }}</text>
</view>
<text class="goods-sold">{{ item.sale }}人付款</text>
</view>
</view>
</view>
</view>
</view>
<!-- 加载更多 -->
<view v-if="leftList.length>0" class="load-more">
<up-loadmore :status="status" />
</view>
<view v-else class="empty-state">
<image :src="Service.GetpayImg('/static/userFunc/null.png')" class="empty-image" mode="aspectFit"></image>
<text class="empty-title">暂无商品</text>
</view>
</view>
<!-- 底部占位 -->
<view class="bottom-placeholder"></view>
</view>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { onLoad } from '@dcloudio/uni-app';
import { Service } from '@/Service/Service';
import { GoodsService } from '@/Service/api/GoodsService'
// 搜索值
let searchValue = ref('');
let status = ref('nomore');
// 轮播图数据
let swiperList = ref<Array<any>>([]);
// 优选商品数据
let scrollList =ref<Array<any>>([]);
// 商品数据
let leftList = ref<Array<any>>([]);
let rightList = ref<Array<any>>([]);
//公告数据
let gonggao=ref<any>({});
let page = ref(1) // 当前页码
onLoad(() => {
getData()
});
const getData = () => {
rightList.value = []
leftList.value = []
page.value = 1
status.value = 'loadmore'
getList()
}
/**
* 加载商品列表数据
* 包含轮播图、优选商品和商品列表
*/
const getList = () => {
// 防止重复加载
if (status.value == 'loading' || status.value == 'nomore') {
return
}
status.value = 'loading'
// 调用API获取商品数据
GoodsService.GetGoodsList( page.value).then(res => {
if (res.code == 0) {
// 第一页时加载轮播图
if (page.value == 1) {
swiperList.value=[]
res.data.bannerList.map((item : any) => {
swiperList.value.push(Service.GetIconImg(item.url))
})
}
// 第一页时加载优选商品
if (page.value == 1) {
scrollList.value = res.data.customsGoods
}
// 第一页时加载优选商品
if (page.value == 1) {
gonggao.value = res.data.gonggao
}
// 将商品列表分为左右两列
res.data.goodsList.map((item : any, index : any) => {
if (index % 2 == 1) {
rightList.value.push(item)
} else {
leftList.value.push(item)
}
})
// 更新加载状态
status.value = res.data.goodsList.length == 10 ? 'loadmore' : 'nomore'
page.value++
} else {
Service.Msg(res.msg)
}
})
}
const goGoodsDetail = (goodsId : number) => {
Service.GoPage('/pages/webView/goodsDetail?goodsId=' + goodsId);
// Service.GoPage('/pages/webView/goodsDetail?goodsId=' + goodsId);
};
const onCategoryClick = (item : any) => {
Service.Msg('点击了 ' + item.name);
};
</script>
<style scoped lang="scss">
page {
background-color: #f4f4f4;
}
.home-page {
position: relative;
}
/* 搜索栏 */
.search-bar {
position: relative;
z-index: 10;
padding: 20rpx 30rpx;
padding-top: 60rpx;
display: flex;
align-items: center;
gap: 16rpx;
}
.search-box {
flex: 1;
display: flex;
align-items: center;
background-color: #fff;
border-radius: 40rpx;
padding: 14rpx 24rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.1);
}
.search-center {
flex: 1;
display: flex;
align-items: center;
margin-left: 12rpx;
gap: 12rpx;
}
.search-placeholder {
color: #999;
font-size: 28rpx;
}
.search-hot {
font-size: 20rpx;
color: #FF6A00;
background: rgba(255, 106, 0, 0.1);
padding: 2rpx 10rpx;
border-radius: 8rpx;
}
.search-msg {
position: relative;
width: 64rpx;
height: 64rpx;
display: flex;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.2);
border-radius: 50%;
backdrop-filter: blur(10rpx);
}
.msg-badge {
position: absolute;
top: 0;
right: 0;
background: #FF3D3D;
color: #fff;
font-size: 18rpx;
width: 28rpx;
height: 28rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
}
/* 主内容 */
.main-content {
position: relative;
z-index: 5;
padding: 0 20rpx;
}
/* 轮播图 */
.swiper-wrap {
margin-top: 10rpx;
border-radius: 16rpx;
overflow: hidden;
box-shadow: 0 8rpx 24rpx rgba(255, 106, 0, 0.15);
}
/* 公告栏 */
.notice-bar {
display: flex;
align-items: center;
background: #fff;
border-radius: 16rpx;
padding: 0 20rpx;
margin-top: 20rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
}
.notice-scroll {
flex: 1;
margin-left: 12rpx;
overflow: hidden;
}
.notice-text {
font-size: 24rpx;
color: #333;
white-space: nowrap;
}
.notice-more {
font-size: 22rpx;
color: #FF6A00;
margin-left: 12rpx;
font-weight: 500;
}
/* 分类宫格 */
.category-grid {
display: flex;
flex-wrap: wrap;
background-color: #fff;
border-radius: 20rpx;
padding: 30rpx 10rpx 20rpx;
margin-top: 20rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.04);
}
.category-item {
width: 20%;
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 20rpx;
transition: transform 0.2s;
}
.category-item:active {
transform: scale(0.95);
}
.category-icon-wrapper {
width: 84rpx;
height: 84rpx;
border-radius: 24rpx;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 10rpx;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.12);
}
.category-name {
font-size: 24rpx;
color: #444;
font-weight: 500;
}
/* 活动入口 */
.activity-bar {
display: flex;
justify-content: space-between;
margin-top: 20rpx;
gap: 12rpx;
}
.activity-item {
flex: 1;
border-radius: 20rpx;
padding: 20rpx;
position: relative;
overflow: hidden;
min-height: 160rpx;
display: flex;
flex-direction: column;
justify-content: space-between;
box-shadow: 0 6rpx 16rpx rgba(0, 0, 0, 0.08);
}
.activity-content {
position: relative;
z-index: 2;
}
.activity-title {
font-size: 28rpx;
font-weight: bold;
color: #fff;
position: relative;
z-index: 2;
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.1);
}
.activity-desc {
font-size: 22rpx;
color: rgba(255, 255, 255, 0.95);
margin-top: 6rpx;
position: relative;
z-index: 2;
background: rgba(255, 255, 255, 0.2);
display: inline-block;
padding: 2rpx 10rpx;
border-radius: 8rpx;
}
.activity-img {
position: absolute;
bottom: -10rpx;
right: -10rpx;
width: 110rpx;
height: 110rpx;
opacity: 0.25;
z-index: 1;
}
.activity-tag {
position: absolute;
top: 0;
right: 0;
background: rgba(255, 255, 255, 0.9);
color: #FF3D3D;
font-size: 18rpx;
font-weight: bold;
padding: 4rpx 12rpx;
border-radius: 0 20rpx 0 12rpx;
z-index: 3;
}
/* 优选商品 */
.section-card {
background-color: #fff;
border-radius: 20rpx;
padding: 24rpx 20rpx;
margin-top: 20rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.04);
}
.section-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 20rpx;
}
.header-left {
display: flex;
align-items: center;
}
.section-icon {
width: 36rpx;
height: 36rpx;
border-radius: 50%;
background: linear-gradient(to right, #FF3D3D, #FF6A00);
display: flex;
align-items: center;
justify-content: center;
margin-right: 10rpx;
}
.section-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
margin-left: 6rpx;
}
.header-right {
display: flex;
align-items: center;
}
.section-more {
font-size: 24rpx;
color: #999;
margin-right: 4rpx;
}
.preferred-scroll {
white-space: nowrap;
}
.preferred-item {
display: inline-block;
width: 240rpx;
margin-right: 20rpx;
vertical-align: top;
}
.preferred-img-wrap {
position: relative;
width: 240rpx;
height: 240rpx;
border-radius: 16rpx;
overflow: hidden;
background-color: #f5f5f5;
}
.preferred-img {
width: 240rpx;
height: 240rpx;
border-radius: 16rpx;
background-color: #f5f5f5;
}
.preferred-discount {
position: absolute;
top: 0;
left: 0;
background: linear-gradient(to right, #FF3D3D, #FF6A00);
color: #fff;
font-size: 20rpx;
font-weight: bold;
padding: 4rpx 12rpx;
border-radius: 16rpx 0 16rpx 0;
z-index: 2;
}
.preferred-name {
font-size: 24rpx;
color: #333;
margin-top: 12rpx;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-weight: 500;
}
.preferred-price-row {
margin-top: 8rpx;
display: flex;
align-items: baseline;
gap: 8rpx;
}
.preferred-price {
font-size: 30rpx;
font-weight: bold;
color: #FF3D3D;
}
.preferred-old {
font-size: 22rpx;
color: #bbb;
text-decoration: line-through;
}
.preferred-progress {
margin-top: 10rpx;
}
.progress-bar {
width: 100%;
height: 12rpx;
background: #f0f0f0;
border-radius: 6rpx;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: linear-gradient(to right, #FF3D3D, #FF6A00);
border-radius: 6rpx;
transition: width 0.5s ease;
}
.progress-text {
font-size: 18rpx;
color: #999;
margin-top: 4rpx;
}
/* 猜你喜欢 */
.guess-header {
display: flex;
align-items: center;
justify-content: center;
margin-top: 36rpx;
margin-bottom: 24rpx;
gap: 12rpx;
}
.guess-icon {
width: 36rpx;
height: 36rpx;
border-radius: 50%;
background: linear-gradient(to right, #FF6A00, #FF9A56);
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2rpx 8rpx rgba(255, 106, 0, 0.3);
}
.guess-line {
width: 60rpx;
height: 2rpx;
background: linear-gradient(to right, transparent, #ddd);
}
.guess-title {
font-size: 30rpx;
color: #333;
font-weight: bold;
}
/* 商品瀑布流 */
.goods-waterfall {
margin-top: 20rpx;
display: flex;
justify-content: space-between;
}
.waterfall-col {
width: 48.5%;
display: flex;
flex-direction: column;
}
.goods-card {
background-color: #fff;
border-radius: 20rpx;
overflow: hidden;
margin-bottom: 20rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
transition: transform 0.2s, box-shadow 0.2s;
}
.goods-card:active {
transform: scale(0.98);
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.1);
}
.goods-img-wrap {
position: relative;
width: 100%;
overflow: hidden;
}
.goods-img {
width: 100%;
display: block;
background-color: #f5f5f5;
}
.goods-label {
position: absolute;
top: 0;
left: 0;
background: linear-gradient(135deg, #FF6A00, #FF4500);
color: #fff;
font-size: 20rpx;
font-weight: 600;
padding: 6rpx 14rpx;
border-radius: 0 0 16rpx 0;
z-index: 1;
}
.goods-rank {
position: absolute;
top: 12rpx;
left: 12rpx;
background: rgba(0, 0, 0, 0.6);
color: #FFD700;
font-size: 20rpx;
font-weight: bold;
padding: 4rpx 12rpx;
border-radius: 8rpx;
display: flex;
align-items: center;
gap: 4rpx;
backdrop-filter: blur(4rpx);
}
.goods-info {
padding: 18rpx;
}
.goods-name {
font-size: 26rpx;
color: #333;
line-height: 1.5;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
text-overflow: ellipsis;
font-weight: 500;
}
.goods-tags {
display: flex;
flex-wrap: wrap;
margin-top: 10rpx;
gap: 8rpx;
}
.tag-text {
font-size: 20rpx;
color: #FF6A00;
border: 1rpx solid rgba(255, 106, 0, 0.3);
padding: 2rpx 10rpx;
border-radius: 8rpx;
background: rgba(255, 106, 0, 0.05);
}
.goods-price-row {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 14rpx;
}
.price-wrap {
display: flex;
align-items: baseline;
gap: 8rpx;
}
.goods-price {
font-size: 24rpx;
color: #FF3D3D;
font-weight: bold;
}
.price-num {
font-size: 34rpx;
}
.goods-old {
font-size: 22rpx;
color: #bbb;
text-decoration: line-through;
}
.goods-sold {
font-size: 20rpx;
color: #999;
}
.goods-shop {
display: flex;
align-items: center;
margin-top: 12rpx;
gap: 6rpx;
}
.shop-tag {
font-size: 18rpx;
color: #fff;
background: linear-gradient(to right, #FF3D3D, #FF6A00);
padding: 2rpx 8rpx;
border-radius: 6rpx;
font-weight: 500;
}
.shop-name {
font-size: 22rpx;
color: #999;
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* 加载更多 */
.load-more {
padding: 30rpx 0;
}
/* 空状态 */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 80rpx 40rpx;
margin: 20rpx 30rpx 0;
background: #fff;
border-radius: 24rpx;
box-shadow: 0 4rpx 24rpx rgba(0, 0, 0, 0.04);
}
.empty-image {
width: 260rpx;
height: 260rpx;
margin-bottom: 16rpx;
opacity: 0.85;
}
.empty-title {
font-size: 32rpx;
color: #333;
font-weight: 600;
margin-bottom: 12rpx;
}
.empty-desc {
font-size: 26rpx;
color: #999;
}
.bottom-placeholder {
width: 100%;
height: 120rpx;
}
</style>

545
src/pages/index/my.vue Normal file
View File

@@ -0,0 +1,545 @@
<template>
<view class="user-page">
<!-- 用户信息卡片 -->
<view class="user-info-card">
<view class="user-header">
<view class="user-info">
<view class="avatar-wrapper">
<image :src="Service.GetMateUrlByImg(info.avatar) "
class="avatar"></image>
</view>
<view class="user-details">
<text class="username">{{ info.nick || '用户昵称' }}</text>
<text class="member-rights">{{ phone(info.phone) || '138****8888' }}</text>
</view>
<view @click="Service.GoPage('/pages/userFunc/setuserData')" class="edit-profile">
<text>编辑</text>
<up-icon name="arrow-right" color="#fff" size="10"></up-icon>
</view>
</view>
</view>
</view>
<!-- 我的订单 -->
<view class="section-card" style="margin-top: 24rpx;">
<view class="section-header">
<view class="section-title-wrap">
<text class="section-title">我的订单</text>
</view>
<view @click="Service.GoPage('/pages/webView/orderList?type=0')" class="view-all">
<text>全部订单</text>
<view class="view-all-arrow">
<up-icon name="arrow-right" color="#fff" size="10"></up-icon>
</view>
</view>
</view>
<view class="order-types">
<view @click="Service.GoPage('/pages/webView/orderList?type=' + (index + 1))"
v-for="(item, index) in orderList" :key="index" class="order-type" hover-class="order-type-active">
<view class="order-icon-wrapper">
<view class="order-icon-bg" :class="'order-icon-bg--' + index">
<image :src="Service.GetpayImg(item.img)" class="order-icon"></image>
</view>
<view v-if="item.count > 0" class="order-badge">{{ item.count > 99 ? '99+' : item.count }}
</view>
</view>
<text class="order-text">{{ item.name }}</text>
</view>
</view>
</view>
<!-- 功能网格 -->
<view class="section-card">
<view class="section-header">
<view class="section-title-wrap">
<text class="section-title">我的功能</text>
</view>
</view>
<view class="service-list">
<view @click="goPage(item.path)" class="service-list-item" v-for="(item, index) in myService"
:key="index" hover-class="service-item-active">
<view class="service-list-left">
<view class="service-list-icon-wrap" :class="'service-list-icon-wrap--' + index">
<image :src="Service.GetpayImg(item.img)" class="order-icon"></image>
</view>
<text class="service-list-text">{{ item.name }}</text>
</view>
<up-icon name="arrow-right" color="#D1D5DB" size="14"></up-icon>
</view>
</view>
</view>
<view class="" style="width: 100%; height: 50rpx;" >
</view>
</view>
</template>
<script setup lang="ts">
import { onShow } from "@dcloudio/uni-app";
import { ref } from "vue";
import { Service } from "@/Service/Service"
import { UserService } from "@/Service/api/UserService"
let info = ref<any>({})
let walletAmount = ref<string>('0.00')
let collectCount = ref<number>(0)
let promoteCount = ref<number>(0)
let orderList = ref([
{
img: '/static/icon/my/pay.png',
name: '待付款',
count: 0
},
{
img: '/static/icon/my/send.png',
name: '待发货',
count: 0
},
{
img: '/static/icon/my/get.png',
name: '待收货',
count: 0
},
{
img: '/static/icon/my/message.png',
name: '待评价',
count: 0
}
])
let myService = ref([
{
img: '/static/icon/my/service/card.png',
name: '钱包',
path: '/pages/userFunc/wallet'
},
{
img: '/static/icon/my/service/ping.png',
name: '爆品团',
path: '/pages/userFunc/Groupsharing'
},
{
img: '/static/icon/my/service/promote.png',
name: '我的推广',
path: '/pages/userFunc/promote'
},
{
img: '/static/icon/my/service/select.png',
name: '收藏夹',
path: '/pages/userFunc/collect'
},
{
img: '/static/icon/my/service/location.png',
name: '收货地址',
path: '/pages/userFunc/addressList',
},
{
img: '/static/icon/my/service/comment.png',
name: '我的评价',
path: '/pages/userFunc/comment',
},
{
img: '/static/icon/my/service/set.png',
name: '设置',
path: '/pages/userFunc/set',
}
])
onShow(() => {
getData()
})
const getData = () => {
UserService.GetUserInfo().then(res => {
if (res.code == 0) {
info.value = res.data.info
orderList.value[0].count = res.data.dfk || 0
orderList.value[1].count = res.data.dfh || 0
orderList.value[2].count = res.data.dsh || 0
orderList.value[3].count = res.data.dpj || 0
} else {
Service.Msg(res.msg)
}
})
}
const phone = (data : any) => {
if (!data) return ''
return String(data).slice(0, 3) + '****' + String(data).slice(-4)
}
const goPage = (path : any) => {
if (path) {
Service.GoPage(path)
}
}
</script>
<style scoped lang="scss">
page {
background-color: #F8F9FA;
overflow: hidden;
}
/* ===== 用户信息模块 ===== */
.user-info-card {
position: relative;
z-index: 1;
margin: 60rpx 30rpx 0;
padding: 30rpx;
background: var(--nav-vice);
backdrop-filter: blur(12rpx);
border-radius: 24rpx;
box-shadow: 0 8rpx 32rpx rgba(249, 115, 22, 0.12);
}
.user-header {
position: relative;
}
.user-info {
display: flex;
align-items: center;
}
.avatar-wrapper {
flex-shrink: 0;
padding: 4rpx;
border-radius: 50%;
background: rgba(255, 255, 255, 0.35);
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.15);
}
.avatar {
width: 120rpx;
height: 120rpx;
border-radius: 50%;
display: block;
background-color: #f5f5f5;
border: 3rpx solid rgba(255, 255, 255, 0.6);
}
.user-details {
margin-left: 24rpx;
flex: 1;
min-width: 0;
}
.username {
font-size: 40rpx;
font-weight: 700;
color: #fff;
letter-spacing: 1rpx;
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.1);
display: block;
}
.member-rights {
font-size: 26rpx;
color: rgba(255, 255, 255, 0.9);
margin-top: 10rpx;
display: block;
font-weight: 400;
}
.edit-profile {
font-size: 22rpx;
color: #fff;
display: flex;
align-items: center;
gap: 6rpx;
flex-shrink: 0;
padding: 10rpx 20rpx;
border-radius: 30rpx;
background: rgba(255, 255, 255, 0.2);
backdrop-filter: blur(8rpx);
border: 1rpx solid rgba(255, 255, 255, 0.3);
}
.edit-profile:active {
background: rgba(255, 255, 255, 0.3);
transform: scale(0.95);
}
/* ===== 数据概览 ===== */
.stats-bar {
position: relative;
z-index: 1;
display: flex;
align-items: center;
justify-content: space-around;
margin: 24rpx 30rpx 0;
padding: 30rpx 0;
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(12rpx);
border-radius: 24rpx;
box-shadow: 0 8rpx 32rpx rgba(249, 115, 22, 0.12);
}
.stats-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 8rpx;
flex: 1;
transition: transform 0.15s;
}
.stats-item:active {
transform: scale(0.92);
}
.stats-num {
font-size: 36rpx;
font-weight: 700;
color: #1F2937;
letter-spacing: 0.5rpx;
}
.stats-label {
font-size: 24rpx;
color: #9CA3AF;
font-weight: 500;
}
.stats-divider {
width: 1rpx;
height: 60rpx;
background: linear-gradient(180deg, transparent, #E5E7EB, transparent);
}
/* ===== 卡片区域 ===== */
.section-card {
background-color: #fff;
margin: 24rpx 30rpx 0;
border-radius: 28rpx;
overflow: hidden;
box-shadow: 0 4rpx 24rpx rgba(0, 0, 0, 0.04);
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20rpx 28rpx 20rpx;
}
.section-title-wrap {
display: flex;
align-items: center;
gap: 14rpx;
}
.section-icon {
width: 48rpx;
height: 48rpx;
border-radius: 14rpx;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
}
.order-section-icon {
background: linear-gradient(135deg, #F97316, #FB923C);
}
.service-section-icon {
background: linear-gradient(135deg, #3B82F6, #60A5FA);
}
.section-title {
font-size: 32rpx;
font-weight: 700;
color: #1F2937;
}
.view-all {
font-size: 24rpx;
color: #fff;
display: flex;
align-items: center;
gap: 6rpx;
padding: 8rpx 18rpx;
border-radius: 30rpx;
background: linear-gradient(135deg, #F97316, #FB923C);
box-shadow: 0 4rpx 12rpx rgba(249, 115, 22, 0.2);
}
.view-all:active {
transform: scale(0.95);
opacity: 0.9;
}
.view-all-arrow {
display: flex;
align-items: center;
justify-content: center;
}
/* ===== 订单区域 ===== */
.order-types {
display: flex;
padding: 10rpx 0 ;
}
.order-type {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
padding: 16rpx 0;
transition: transform 0.15s;
}
.order-type-active {
transform: scale(0.92);
}
.order-icon-wrapper {
position: relative;
display: inline-block;
}
.order-icon-bg {
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
width: 96rpx;
height: 96rpx;
transition: transform 0.2s;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
}
.order-icon-bg--0 {
background: linear-gradient(135deg, #FFF7ED, #FFEDD5);
}
.order-icon-bg--1 {
background: linear-gradient(135deg, #EFF6FF, #DBEAFE);
}
.order-icon-bg--2 {
background: linear-gradient(135deg, #ECFDF5, #D1FAE5);
}
.order-icon-bg--3 {
background: linear-gradient(135deg, #FDF2F8, #FCE7F3);
}
.order-icon {
width: 44rpx;
height: 44rpx;
display: block;
}
.order-badge {
position: absolute;
top: -4rpx;
right: -8rpx;
min-width: 32rpx;
height: 32rpx;
padding: 0 8rpx;
background: linear-gradient(135deg, #FF6A00, #FF4500);
color: #fff;
font-size: 20rpx;
font-weight: 600;
line-height: 32rpx;
text-align: center;
border-radius: 16rpx;
box-shadow: 0 2rpx 8rpx rgba(255, 106, 0, 0.4);
border: 2rpx solid #fff;
}
.order-text {
font-size: 24rpx;
color: #4B5563;
margin-top: 14rpx;
font-weight: 500;
}
/* ===== 服务列表 ===== */
.service-list {
padding: 0 20rpx 20rpx;
}
.service-list-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20rpx 16rpx;
transition: all 0.2s;
border-bottom: 1rpx solid #F3F4F6;
}
.service-list-item:last-child {
border-bottom: none;
}
.service-list-item:active {
background: #F9FAFB;
border-radius: 16rpx;
transform: scale(0.98);
}
.service-list-left {
display: flex;
align-items: center;
gap: 20rpx;
}
.service-list-icon-wrap {
width: 72rpx;
height: 72rpx;
border-radius: 18rpx;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.06);
}
.service-list-icon-wrap--0 {
background: linear-gradient(135deg, #FFF1F2, #FFE4E6);
}
.service-list-icon-wrap--1 {
background: linear-gradient(135deg, #F0FDF4, #DCFCE7);
}
.service-list-icon-wrap--2 {
background: linear-gradient(135deg, #F5F3FF, #EDE9FE);
}
.service-list-icon-wrap--3 {
background: linear-gradient(135deg, #F0F9FF, #E0F2FE);
}
.service-list-icon-wrap--4 {
background: linear-gradient(135deg, #FFFBEB, #FDE68A);
}
.service-list-icon-wrap--5 {
background: linear-gradient(135deg, #ECFEFF, #CFFAFE);
}
.service-list-text {
font-size: 26rpx;
color: #1F2937;
font-weight: 500;
}
.service-item-active {
opacity: 0.8;
}
</style>

522
src/pages/index/shop.vue Normal file
View File

@@ -0,0 +1,522 @@
<template>
<view class="cart-container">
<!-- 购物车标题栏 -->
<view class="cart-header">
<text class="cart-title">我的购物车</text>
<text v-if='!isDele' @click="isDele=true" class="cart-manage">管理</text>
<text v-if='isDele' @click="isDele=false" class="cart-manage">退出管理</text>
</view>
<!-- 购物车商品列表 -->
<scroll-view scroll-y class="cart-items" :show-scrollbar="false">
<view class="cart-item" v-for="(item, index) in cartData" :key="item.cgId">
<view class="" @click.stop="selectItem(index)"
:style="{ 'background-color': selectedIds.includes(item.cgId) ? '#2979ff' : '', 'border': selectedIds.includes(item.cgId) ? '' : '1rpx solid #e2e2e2' }"
style="display: flex; align-items: center; justify-content: center; width: 34rpx; height: 34rpx; border-radius: 4rpx;">
<up-icon name="checkbox-mark" color="#fff" size="14"></up-icon>
</view>
<image :src="Service.GetMateUrlByImg(item.goodsImg)" mode="scaleToFill" class="item-image">
</image>
<view class="item-info" style="position: relative;">
<view class="" style="display: flex; justify-content: space-between;">
<text class="item-title" style="flex: 1;">{{ item.goodsName }}</text>
<image v-if="isDele" @click.stop="deleteItem(item)"
:src="Service.GetpayImg('/static/icon/shop/dele.png')"
style="width: 32rpx; height: 32rpx;" />
</view>
<view class="">
<text v-for="(ruleIndex, ruleItem) in item.rule.split('-')" :key="ruleItem"
class="item-spec">{{ ruleIndex }}</text>
</view>
<view class="item-actions">
<text class="item-price">¥{{ item.price.toFixed(2) }}</text>
<view class="stepper">
<view class="stepper-btn" :class="{ disabled: item.count <= 1 }"
@click.stop="decreaseCount(item)">
-
</view>
<view class="stepper-input">{{ item.count }}</view>
<view class="stepper-btn" @click.stop="increaseCount(item)">
+
</view>
</view>
</view>
</view>
</view>
<!-- 空购物车提示 -->
<view v-if="cartData.length === 0" class="empty-cart">
<image :src="Service.GetpayImg('/static/dele/empty-cart.png')" mode="aspectFit" class="empty-image">
</image>
<text class="empty-text">购物车空空如也</text>
</view>
<!-- 底部空间确保内容不被底部结算栏遮挡 -->
<view class="bottom-space"></view>
</scroll-view>
<!-- 底部结算栏 -->
<view class="checkout-bar">
<view v-if="!isDele" class="select-all" @click="toggleSelectAll">
<view class=""
:style="{ 'background-color': isAllSelected ? '#2979ff' : '', 'border': isAllSelected ? '' : '1rpx solid #e2e2e2' }"
style="display: flex; align-items: center; justify-content: center; width: 34rpx; height: 34rpx; border-radius: 4rpx;">
<up-icon name="checkbox-mark" color="#fff" size="14"></up-icon>
</view>
<text class="select-all-text">全选</text>
</view>
<view v-if="!isDele" class="total-info">
<view class="total-price">
<text class="total-text">合计</text>
<text class="price-text">¥{{ totalPrice.toFixed(2) }}</text>
</view>
<text class="total-count">{{ totalCount }}</text>
</view>
<view v-if="!isDele" class="checkout-btn" @click="handleCheckout">
<text class="checkout-text">结算({{ selectedCount }})</text>
</view>
<view v-if="isDele" style="width: 100%;" class="delete-btn" @click="deleteSelectedItems">
<view class=""
style=" padding: 20rpx 30rpx; border-radius: 10rpx; font-size: 22rpx; background-color: red; color: #fff; display: flex; align-items: center; justify-content: center; ">
删除选中({{ selectedCount }})
</view>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue';
import { Service } from '@/Service/Service';
import { onLoad, onShow } from '@dcloudio/uni-app';
import { CarService } from '@/Service/api/CarService';
import { OrderService } from '@/Service/api/OrderService';
let isDele = ref(false)
let status = ref('nomore')
let page = ref(1)
let selectedIds = ref<string[]>([])
// 购物车数据结构
const cartData = ref<Array<any>>([])
// 计算属性:选中商品列表
const selectedItems = computed(() => {
return cartData.value.filter(item => selectedIds.value.includes(item.cgId))
})
// 计算属性:选中商品总数
const selectedCount = computed(() => {
return selectedItems.value.length
})
// 计算属性:选中商品总数量
const totalCount = computed(() => {
return selectedItems.value.reduce((sum, item) => sum + item.count, 0)
})
// 计算属性:选中商品总价
const totalPrice = computed(() => {
return selectedItems.value.reduce((sum, item) => sum + item.count * item.price, 0)
})
// 计算属性:是否全选
const isAllSelected = computed(() => {
return cartData.value.length > 0 && selectedIds.value.length === cartData.value.length
})
onLoad(() => {
})
onShow(() => {
selectedIds.value = []
getData()
})
// 获取商品
const getData = () => {
cartData.value = []
status.value = 'loadmore'
page.value = 1
getList()
}
const getList = () => {
if (status.value == 'loading' || status.value == 'nomore') {
return
}
status.value = 'loading'
CarService.GetUserCarList(page.value).then(res => {
if (res.code == 0) {
cartData.value = [...cartData.value, ...res.data.list]
status.value = res.data.list.length == 10 ? 'loadmore' : 'nomore'
page.value++
} else {
Service.Msg(res.msg)
}
})
}
// 选择/取消选择单个商品
const selectItem = (index: number) => {
const cgId = cartData.value[index].cgId
const idx = selectedIds.value.indexOf(cgId)
if (idx > -1) {
selectedIds.value.splice(idx, 1)
} else {
selectedIds.value.push(cgId)
}
}
// 全选/取消全选
const toggleSelectAll = () => {
if (isAllSelected.value) {
selectedIds.value = []
} else {
selectedIds.value = cartData.value.map(item => item.cgId)
}
}
// 增加商品数量
const increaseCount = (shop : any) => {
shop.count++
updateItemCount(shop.cgId, shop.count)
}
// 减少商品数量
const decreaseCount = (shop : any) => {
if (shop.count <= 1) {
return
}
shop.count--
updateItemCount(shop.cgId, shop.count)
}
// 更新商品数量
const updateItemCount = (cgId : string, count : number) => {
CarService.UpdateCarCount(cgId, count).then(res => {
if (res.code == 0) {
} else {
Service.Msg(res.msg)
}
})
}
// 删除单个商品
const deleteItem = (goods : any) => {
uni.showModal({
title: '提示',
content: '确定要删除该商品吗?',
success: (res) => {
if (res.confirm) {
CarService.DelUserCar(goods.goodsId, 1).then(res => {
if (res.code == 0) {
Service.Msg(res.data)
getData()
selectedIds.value = []
isDele.value = false
} else {
Service.Msg(res.msg)
}
})
}
}
})
}
// 删除选中商品
const deleteSelectedItems = () => {
if (selectedIds.value.length === 0) {
Service.Msg('请选择要删除的商品')
return
}
uni.showModal({
title: '提示',
content: `确定要删除选中的${selectedIds.value.length}件商品吗?`,
success: (res) => {
if (res.confirm) {
const goodsIds = selectedItems.value.map(item => item.goodsId).join(',')
CarService.DelUserCar(goodsIds, 2).then(res => {
if (res.code == 0) {
Service.Msg(res.data)
selectedIds.value = []
getData()
isDele.value = false
} else {
Service.Msg(res.msg)
}
})
} else {
isDele.value = false
}
}
})
}
// 结算按钮点击处理
const handleCheckout = () => {
if (selectedCount.value === 0) {
Service.Msg('请选择要结算的商品')
return
}
const data = selectedItems.value.map(item => ({
goodsId: item.goodsId,
goodsImg: item.goodsImg,
goodsName: item.goodsName,
rule: item.rule,
count: item.count,
price: item.price
}))
OrderService.AddUserOrder(JSON.stringify(data), 0).then(res => {
if (res.code == 0) {
Service.GoPage('/pages/webView/orderDetail?orderId=' + res.data.orderId)
} else {
Service.Msg(res.msg)
}
})
}
</script>
<style scoped>
.cart-container {
width: 100%;
height: 100vh;
display: flex;
flex-direction: column;
background-color: #f5f5f5;
}
/* 购物车标题栏 */
.cart-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 24rpx 32rpx;
background-color: #ffffff;
font-size: 32rpx;
font-weight: bold;
color: #333333;
}
.cart-title {
font-size: 36rpx;
}
.cart-manage {
font-size: 28rpx;
color: #666666;
font-weight: normal;
}
/* 购物车商品列表 */
.cart-items {
flex: 1;
margin-top: 20rpx;
}
/* 商品项 */
.cart-item {
display: flex;
align-items: center;
padding: 24rpx 32rpx;
margin: 0 20rpx 10rpx;
background-color: #ffffff;
border-radius: 20rpx;
}
.item-image {
width: 160rpx;
height: 160rpx;
margin-left: 16rpx;
border-radius: 8rpx;
overflow: hidden;
}
.item-info {
flex: 1;
margin-left: 24rpx;
}
.item-title {
font-size: 28rpx;
color: #333333;
line-height: 1.4;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
}
.item-spec {
font-size: 24rpx;
color: #999999;
margin-right: 10rpx;
margin-top: 8rpx;
}
.item-actions {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 16rpx;
}
.item-price {
font-size: 32rpx;
color: #ff6600;
font-weight: bold;
}
/* 手写步进器样式 */
.stepper {
display: flex;
align-items: center;
border: 1rpx solid #e0e0e0;
border-radius: 8rpx;
overflow: hidden;
}
.stepper-btn {
width: 56rpx;
height: 52rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 36rpx;
color: #333;
background-color: #f8f8f8;
transition: background-color 0.2s;
}
.stepper-btn:active {
background-color: #e8e8e8;
}
.stepper-btn.disabled {
color: #ccc;
background-color: #f5f5f5;
}
.stepper-input {
width: 80rpx;
height: 56rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 28rpx;
color: #333;
background-color: #fff;
border-left: 1rpx solid #e0e0e0;
border-right: 1rpx solid #e0e0e0;
}
/* 空购物车 */
.empty-cart {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 100rpx 0;
}
.empty-image {
width: 300rpx;
height: 300rpx;
margin-bottom: 30rpx;
}
.empty-text {
font-size: 28rpx;
color: #999;
}
/* 底部空间 */
.bottom-space {
height: 220rpx;
}
/* 底部结算栏 */
.checkout-bar {
position: fixed;
bottom: 0rpx;
left: 0;
right: 0;
height: 100rpx;
display: flex;
justify-content: space-between;
align-items: center;
background-color: #ffffff;
border-top: 1rpx solid #f0f0f0;
padding: 0 32rpx;
}
.select-all {
display: flex;
align-items: center;
}
.select-all-text {
font-size: 28rpx;
color: #333333;
margin-left: 16rpx;
}
.total-info {
flex: 1;
margin-left: 32rpx;
}
.total-price {
display: flex;
align-items: baseline;
}
.total-text {
font-size: 28rpx;
color: #333333;
}
.price-text {
font-size: 36rpx;
color: #ff6600;
font-weight: bold;
}
.total-count {
font-size: 24rpx;
color: #999999;
}
.checkout-btn {
width: 200rpx;
height: 72rpx;
background-color: #ff6600;
border-radius: 36rpx;
display: flex;
align-items: center;
justify-content: center;
}
.checkout-text {
font-size: 28rpx;
color: #ffffff;
font-weight: bold;
}
.delete-btn {
cursor: pointer;
}
</style>

View File

@@ -0,0 +1,284 @@
<template>
<view class="set-paypwd-page">
<view class="set-paypwd-container">
<view class="set-paypwd-header">
<text class="page-title">设置支付密码</text>
<text class="page-subtitle">请输入手机号和验证码设置支付密码</text>
</view>
<view class="set-paypwd-form">
<view class="input-item">
<up-icon name="phone" size="20" color="#999"></up-icon>
<input v-model="passwordForm.phone" class="input-field" type="number" maxlength="11"
placeholder="请输入手机号" />
</view>
<view class="input-item">
<up-icon name="error-circle" size="20" color="#999"></up-icon>
<input v-model="passwordForm.code" class="input-field code-input" type="number" maxlength="6"
placeholder="请输入验证码" />
<button class="code-btn" :disabled="counting" @click="getCode">
{{ counting ? `${countdown}s` : '获取验证码' }}
</button>
</view>
<view class="input-item">
<up-icon name="lock" size="20" color="#999"></up-icon>
<input v-model="passwordForm.password" class="input-field" :type="showPassword ? 'text' : 'password'"
maxlength="6" placeholder="请输入支付密码(6位数字)" />
<view class="password-toggle" @click="togglePassword">
<up-icon :name="showPassword ? 'eye' : 'eye-off'" size="18" color="#999"></up-icon>
</view>
</view>
<view class="input-item">
<up-icon name="lock" size="20" color="#999"></up-icon>
<input v-model="passwordForm.confirmPassword" class="input-field" :type="showConfirmPassword ? 'text' : 'password'"
maxlength="6" placeholder="请确认支付密码" />
<view class="password-toggle" @click="toggleConfirmPassword">
<up-icon :name="showConfirmPassword ? 'eye' : 'eye-off'" size="18" color="#999"></up-icon>
</view>
</view>
<button class="confirm-btn" @click="confirmSet">
确认设置
</button>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { Service } from '@/Service/Service';
import { onLoad } from '@dcloudio/uni-app';
import { LoginService } from "@/Service/api/LoginService"
import { UserService } from "@/Service/api/UserService"
const counting = ref(false);
const countdown = ref(60);
const showPassword = ref(false);
const showConfirmPassword = ref(false);
const passwordForm = ref({
phone: '',
code: '',
password: '',
confirmPassword: ''
});
onLoad(() => {
});
const togglePassword = () => {
showPassword.value = !showPassword.value;
};
const toggleConfirmPassword = () => {
showConfirmPassword.value = !showConfirmPassword.value;
};
const getCode = () => {
if (!passwordForm.value.phone) {
Service.Msg('请输入手机号');
return;
}
const phoneReg = /^1[3-9]\d{9}$/;
if (!phoneReg.test(passwordForm.value.phone)) {
Service.Msg('请输入正确的手机号');
return;
}
counting.value = true;
LoginService.SendSms(passwordForm.value.phone, 'ForgetPayPwd').then(res => {
if (res.code == 0) {
Service.Msg('验证码已发送!');
} else {
Service.Msg(res.msg);
}
});
const timer = setInterval(() => {
countdown.value--;
if (countdown.value <= 0) {
clearInterval(timer);
counting.value = false;
countdown.value = 60;
}
}, 1000);
};
const confirmSet = () => {
if (!passwordForm.value.phone) {
Service.Msg('请输入手机号');
return;
}
const phoneReg = /^1[3-9]\d{9}$/;
if (!phoneReg.test(passwordForm.value.phone)) {
Service.Msg('请输入正确的手机号');
return;
}
if (!passwordForm.value.code) {
Service.Msg('请输入验证码');
return;
}
if (!passwordForm.value.password) {
Service.Msg('请输入支付密码');
return;
}
if (passwordForm.value.password.length !== 6) {
Service.Msg('支付密码必须是6位数字');
return;
}
if (!/^\d+$/.test(passwordForm.value.password)) {
Service.Msg('支付密码只能是数字');
return;
}
if (!passwordForm.value.confirmPassword) {
Service.Msg('请确认支付密码');
return;
}
if (passwordForm.value.password !== passwordForm.value.confirmPassword) {
Service.Msg('两次密码输入不一致');
return;
}
Service.LoadIng('设置中...');
let data = {
phone: passwordForm.value.phone,
code: passwordForm.value.code,
pwd: passwordForm.value.password,
npwd: passwordForm.value.confirmPassword
}
UserService.ForgetPayPwd(data).then(res => {
Service.LoadClose();
if(res.code == 0){
Service.Msg('支付密码设置成功!');
setTimeout(() => {
Service.GoPageBack()
}, 1000)
}else{
Service.Msg(res.msg)
}
})
};
</script>
<style scoped lang="scss">
.set-paypwd-page {
min-height: 100vh;
background: linear-gradient(135deg, #FFF7ED 0%, #FFEDD5 100%);
padding: 40rpx;
}
.set-paypwd-container {
width: 100%;
background-color: #FFFFFF;
border-radius: 32rpx;
padding: 60rpx 40rpx;
box-shadow: 0 8rpx 32rpx rgba(249, 115, 22, 0.1);
}
.set-paypwd-header {
margin-bottom: 50rpx;
}
.page-title {
display: block;
font-size: 44rpx;
font-weight: bold;
color: #1F2937;
margin-bottom: 10rpx;
}
.page-subtitle {
font-size: 28rpx;
color: #6B7280;
}
.set-paypwd-form {
margin-bottom: 30rpx;
}
.input-item {
display: flex;
align-items: center;
background-color: #F9FAFB;
border-radius: 16rpx;
padding: 24rpx 30rpx;
margin-bottom: 28rpx;
border: 2rpx solid transparent;
transition: all 0.3s;
&:focus-within {
border-color: #F97316;
background-color: #FFF;
}
}
.input-field {
flex: 1;
margin-left: 20rpx;
font-size: 30rpx;
color: #1F2937;
}
.code-input {
flex: 1;
}
.code-btn {
margin-left: 20rpx;
padding: 16rpx 24rpx;
background: linear-gradient(135deg, #F97316, #FB923C);
color: #FFFFFF;
font-size: 26rpx;
border-radius: 12rpx;
border: none;
line-height: 1;
white-space: nowrap;
&:disabled {
opacity: 0.6;
color: #fff;
}
}
.password-toggle {
width: 50rpx;
height: 50rpx;
display: flex;
align-items: center;
justify-content: center;
margin-left: 10rpx;
}
.confirm-btn {
width: 100%;
padding: 0 30rpx;
background: linear-gradient(135deg, #F97316, #FB923C);
color: #FFFFFF;
font-size: 34rpx;
font-weight: 600;
border: none;
border-radius: 16rpx;
margin-top: 30rpx;
box-shadow: 0 8rpx 24rpx rgba(249, 115, 22, 0.3);
transition: all 0.3s;
&:active {
transform: scale(0.98);
box-shadow: 0 4rpx 12rpx rgba(249, 115, 22, 0.2);
}
}
</style>

View File

@@ -0,0 +1,214 @@
<template>
<view class="change-phone-page">
<view class="change-phone-container">
<view class="change-phone-header">
<text class="page-title">修改手机号</text>
<text class="page-subtitle">请输入新的手机号码进行绑定</text>
</view>
<view class="change-phone-form">
<view class="input-item">
<up-icon name="phone" size="20" color="#999"></up-icon>
<input v-model="phoneForm.phone" class="input-field" type="number" maxlength="11"
placeholder="请输入新手机号" />
</view>
<view class="input-item">
<up-icon name="error-circle" size="20" color="#999"></up-icon>
<input v-model="phoneForm.code" class="input-field code-input" type="number" maxlength="6"
placeholder="请输入验证码" />
<button class="code-btn" :disabled="counting" @click="getCode">
{{ counting ? `${countdown}s` : '获取验证码' }}
</button>
</view>
<button class="confirm-btn" @click="confirmBind">
确认绑定
</button>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { Service } from '@/Service/Service';
import { onLoad } from '@dcloudio/uni-app';
import { LoginService } from "@/Service/api/LoginService"
import { UserService } from "@/Service/api/UserService"
const counting = ref(false);
const countdown = ref(60);
const phoneForm = ref({
phone: '',
code: ''
});
onLoad(() => {
});
const getCode = () => {
if (!phoneForm.value.phone) {
Service.Msg('请输入手机号');
return;
}
const phoneReg = /^1[3-9]\d{9}$/;
if (!phoneReg.test(phoneForm.value.phone)) {
Service.Msg('请输入正确的手机号');
return;
}
counting.value = true;
LoginService.SendSms(phoneForm.value.phone, 'UpdatePhone').then(res => {
if (res.code == 0) {
Service.Msg('验证码已发送!');
} else {
Service.Msg(res.msg);
}
});
const timer = setInterval(() => {
countdown.value--;
if (countdown.value <= 0) {
clearInterval(timer);
counting.value = false;
countdown.value = 60;
}
}, 1000);
};
const confirmBind = () => {
if (!phoneForm.value.phone) {
Service.Msg('请输入手机号');
return;
}
const phoneReg = /^1[3-9]\d{9}$/;
if (!phoneReg.test(phoneForm.value.phone)) {
Service.Msg('请输入正确的手机号');
return;
}
if (!phoneForm.value.code) {
Service.Msg('请输入验证码');
return;
}
Service.LoadIng('绑定中...');
UserService.UpdateUserPhone(phoneForm.value.phone,phoneForm.value.code).then(res=>{
if(res.code==0){
Service.LoadClose();
Service.Msg('手机号修改成功');
Service.GoPageBack();
}else{
Service.Msg(res.msg);
}
})
};
</script>
<style scoped lang="scss">
.change-phone-page {
min-height: 100vh;
background: linear-gradient(135deg, #FFF7ED 0%, #FFEDD5 100%);
padding: 40rpx;
}
.change-phone-container {
width: 100%;
background-color: #FFFFFF;
border-radius: 32rpx;
padding: 60rpx 40rpx;
box-shadow: 0 8rpx 32rpx rgba(249, 115, 22, 0.1);
}
.change-phone-header {
margin-bottom: 50rpx;
}
.page-title {
display: block;
font-size: 44rpx;
font-weight: bold;
color: #1F2937;
margin-bottom: 10rpx;
}
.page-subtitle {
font-size: 28rpx;
color: #6B7280;
}
.change-phone-form {
margin-bottom: 30rpx;
}
.input-item {
display: flex;
align-items: center;
background-color: #F9FAFB;
border-radius: 16rpx;
padding: 24rpx 30rpx;
margin-bottom: 28rpx;
border: 2rpx solid transparent;
transition: all 0.3s;
&:focus-within {
border-color: #F97316;
background-color: #FFF;
}
}
.input-field {
flex: 1;
margin-left: 20rpx;
font-size: 30rpx;
color: #1F2937;
}
.code-input {
flex: 1;
}
.code-btn {
margin-left: 20rpx;
padding: 16rpx 24rpx;
background: linear-gradient(135deg, #F97316, #FB923C);
color: #FFFFFF;
font-size: 26rpx;
border-radius: 12rpx;
border: none;
line-height: 1;
white-space: nowrap;
&:disabled {
opacity: 0.6;
background-color: #fff;
}
}
.confirm-btn {
width: 100%;
padding: 0 30rpx;
background: linear-gradient(135deg, #F97316, #FB923C);
color: #FFFFFF;
font-size: 34rpx;
font-weight: 600;
border: none;
border-radius: 16rpx;
margin-top: 30rpx;
box-shadow: 0 8rpx 24rpx rgba(249, 115, 22, 0.3);
transition: all 0.3s;
&:active {
transform: scale(0.98);
box-shadow: 0 4rpx 12rpx rgba(249, 115, 22, 0.2);
}
}
</style>

View File

@@ -0,0 +1,276 @@
<template>
<view class="change-password-page">
<view class="change-password-container">
<view class="change-password-header">
<text class="page-title">修改密码</text>
<text class="page-subtitle">请输入手机号和验证码设置新密码</text>
</view>
<view class="change-password-form">
<view class="input-item">
<up-icon name="phone" size="20" color="#999"></up-icon>
<input v-model="passwordForm.phone" class="input-field" type="number" maxlength="11"
placeholder="请输入手机号" />
</view>
<view class="input-item">
<up-icon name="error-circle" size="20" color="#999"></up-icon>
<input v-model="passwordForm.code" class="input-field code-input" type="number" maxlength="6"
placeholder="请输入验证码" />
<button class="code-btn" :disabled="counting" @click="getCode">
{{ counting ? `${countdown}s` : '获取验证码' }}
</button>
</view>
<view class="input-item">
<up-icon name="lock" size="20" color="#999"></up-icon>
<input v-model="passwordForm.password" class="input-field" :type="showPassword ? 'text' : 'password'"
placeholder="请输入新密码" />
<view class="password-toggle" @click="togglePassword">
<up-icon :name="showPassword ? 'eye' : 'eye-off'" size="18" color="#999"></up-icon>
</view>
</view>
<view class="input-item">
<up-icon name="lock" size="20" color="#999"></up-icon>
<input v-model="passwordForm.confirmPassword" class="input-field" :type="showConfirmPassword ? 'text' : 'password'"
placeholder="请确认新密码" />
<view class="password-toggle" @click="toggleConfirmPassword">
<up-icon :name="showConfirmPassword ? 'eye' : 'eye-off'" size="18" color="#999"></up-icon>
</view>
</view>
<button class="confirm-btn" @click="confirmChange">
确认修改
</button>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { Service } from '@/Service/Service';
import { onLoad } from '@dcloudio/uni-app';
import { LoginService } from "@/Service/api/LoginService"
const counting = ref(false);
const countdown = ref(60);
const showPassword = ref(false);
const showConfirmPassword = ref(false);
const passwordForm = ref({
phone: '',
code: '',
password: '',
confirmPassword: ''
});
onLoad(() => {
});
const togglePassword = () => {
showPassword.value = !showPassword.value;
};
const toggleConfirmPassword = () => {
showConfirmPassword.value = !showConfirmPassword.value;
};
const getCode = () => {
if (!passwordForm.value.phone) {
Service.Msg('请输入手机号');
return;
}
const phoneReg = /^1[3-9]\d{9}$/;
if (!phoneReg.test(passwordForm.value.phone)) {
Service.Msg('请输入正确的手机号');
return;
}
counting.value = true;
LoginService.SendSms(passwordForm.value.phone, 'ForgetPwd').then(res => {
if (res.code == 0) {
Service.Msg('验证码已发送!');
} else {
Service.Msg(res.msg);
}
});
const timer = setInterval(() => {
countdown.value--;
if (countdown.value <= 0) {
clearInterval(timer);
counting.value = false;
countdown.value = 60;
}
}, 1000);
};
const confirmChange = () => {
if (!passwordForm.value.phone) {
Service.Msg('请输入手机号');
return;
}
const phoneReg = /^1[3-9]\d{9}$/;
if (!phoneReg.test(passwordForm.value.phone)) {
Service.Msg('请输入正确的手机号');
return;
}
if (!passwordForm.value.code) {
Service.Msg('请输入验证码');
return;
}
if (!passwordForm.value.password) {
Service.Msg('请输入新密码');
return;
}
if (passwordForm.value.password.length < 6) {
Service.Msg('密码长度不能少于6位');
return;
}
if (!passwordForm.value.confirmPassword) {
Service.Msg('请确认新密码');
return;
}
if (passwordForm.value.password !== passwordForm.value.confirmPassword) {
Service.Msg('两次密码输入不一致');
return;
}
Service.LoadIng('修改中...');
let data={
phone:passwordForm.value.phone,
code:passwordForm.value.code,
pwd:passwordForm.value.password,
npwd:passwordForm.value.confirmPassword
}
LoginService.ForgetPwd(data).then(res=>{
if(res.code==0){
Service.LoadClose();
Service.Msg('密码修改成功!');
Service.GoPageBack()
}else{
Service.Msg(res.msg)
}
})
};
</script>
<style scoped lang="scss">
.change-password-page {
min-height: 100vh;
background: linear-gradient(135deg, #FFF7ED 0%, #FFEDD5 100%);
padding: 40rpx;
}
.change-password-container {
width: 100%;
background-color: #FFFFFF;
border-radius: 32rpx;
padding: 60rpx 40rpx;
box-shadow: 0 8rpx 32rpx rgba(249, 115, 22, 0.1);
}
.change-password-header {
margin-bottom: 50rpx;
}
.page-title {
display: block;
font-size: 44rpx;
font-weight: bold;
color: #1F2937;
margin-bottom: 10rpx;
}
.page-subtitle {
font-size: 28rpx;
color: #6B7280;
}
.change-password-form {
margin-bottom: 30rpx;
}
.input-item {
display: flex;
align-items: center;
background-color: #F9FAFB;
border-radius: 16rpx;
padding: 24rpx 30rpx;
margin-bottom: 28rpx;
border: 2rpx solid transparent;
transition: all 0.3s;
&:focus-within {
border-color: #F97316;
background-color: #FFF;
}
}
.input-field {
flex: 1;
margin-left: 20rpx;
font-size: 30rpx;
color: #1F2937;
}
.code-input {
flex: 1;
}
.code-btn {
margin-left: 20rpx;
padding: 16rpx 24rpx;
background: linear-gradient(135deg, #F97316, #FB923C);
color: #FFFFFF;
font-size: 26rpx;
border-radius: 12rpx;
border: none;
line-height: 1;
white-space: nowrap;
&:disabled {
opacity: 0.6;
}
}
.password-toggle {
width: 50rpx;
height: 50rpx;
display: flex;
align-items: center;
justify-content: center;
margin-left: 10rpx;
}
.confirm-btn {
width: 100%;
padding: 0 30rpx;
background: linear-gradient(135deg, #F97316, #FB923C);
color: #FFFFFF;
font-size: 34rpx;
font-weight: 600;
border: none;
border-radius: 16rpx;
margin-top: 30rpx;
box-shadow: 0 8rpx 24rpx rgba(249, 115, 22, 0.3);
transition: all 0.3s;
&:active {
transform: scale(0.98);
box-shadow: 0 4rpx 12rpx rgba(249, 115, 22, 0.2);
}
}
</style>

View File

@@ -0,0 +1,295 @@
<template>
<view class="forgot-password-page">
<view class="forgot-password-container">
<view class="forgot-password-header">
<!-- <image :src="Service.GetIconImg('/static/logo/logo.png')" class="logo"></image> -->
<text class="app-title">忘记密码</text>
<text class="app-subtitle">请验证您的身份</text>
</view>
<view class="forgot-password-form">
<view class="input-item">
<u-icon name="phone" size="20" color="#999"></u-icon>
<input v-model="formData.phone" class="input-field" type="number" maxlength="11"
placeholder="请输入手机号" />
</view>
<view class="input-item">
<u-icon name="error-circle" size="20" color="#999"></u-icon>
<input v-model="formData.code" class="input-field code-input" type="number" maxlength="6"
placeholder="请输入验证码" />
<button class="code-btn" :disabled="counting" @click="getCode">
{{ counting ? `${countdown}s` : '获取验证码' }}
</button>
</view>
<view class="input-item">
<u-icon name="lock" size="20" color="#999"></u-icon>
<input v-model="formData.newPassword" class="input-field" type="password"
:password="showNewPassword" placeholder="请设置新密码" />
<u-icon :name="showNewPassword ? 'eye-off' : 'eye'" size="20" color="#999"
@click="toggleNewPassword"></u-icon>
</view>
<view class="input-item">
<u-icon name="lock" size="20" color="#999"></u-icon>
<input v-model="formData.confirmPassword" class="input-field" type="password"
:password="showConfirmPassword" placeholder="请确认新密码" />
<u-icon :name="showConfirmPassword ? 'eye-off' : 'eye'" size="20" color="#999"
@click="toggleConfirmPassword"></u-icon>
</view>
<button class="submit-btn" @click="handleSubmit">
确认修改
</button>
<view class="forgot-password-footer">
<text class="footer-text">想起密码了</text>
<text class="footer-link" @click="goToLogin">返回登录</text>
</view>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { Service } from '@/Service/Service';
import { LoginService } from "@/Service/api/LoginService"
const showNewPassword = ref(false);
const showConfirmPassword = ref(false);
const counting = ref(false);
const countdown = ref(60);
const formData = ref({
phone: '',
code: '',
newPassword: '',
confirmPassword: ''
});
const toggleNewPassword = () => {
showNewPassword.value = !showNewPassword.value;
};
const toggleConfirmPassword = () => {
showConfirmPassword.value = !showConfirmPassword.value;
};
const getCode = () => {
if (!formData.value.phone) {
Service.Msg('请输入手机号!');
return;
}
const phoneReg = /^1[3-9]\d{9}$/;
if (!phoneReg.test(formData.value.phone)) {
Service.Msg('请输入正确的手机号!');
return;
}
counting.value = true;
LoginService.SendSms(formData.value.phone,'ForgetPwd').then(res => {
if (res.code == 0) {
Service.Msg('验证码已发送!');
} else {
Service.Msg(res.msg)
}
})
const timer = setInterval(() => {
countdown.value--;
if (countdown.value <= 0) {
clearInterval(timer);
counting.value = false;
countdown.value = 60;
}
}, 1000);
};
const handleSubmit = () => {
if (!formData.value.phone) {
Service.Msg('请输入手机号!');
return;
}
const phoneReg = /^1[3-9]\d{9}$/;
if (!phoneReg.test(formData.value.phone)) {
Service.Msg('请输入正确的手机号!');
return;
}
if (!formData.value.code) {
Service.Msg('请输入验证码!');
return;
}
if (!formData.value.newPassword) {
Service.Msg('请设置新密码!');
return;
}
if (formData.value.newPassword.length < 6) {
Service.Msg('密码长度不能少于6位!');
return;
}
if (!formData.value.confirmPassword) {
Service.Msg('请确认新密码!');
return;
}
if (formData.value.newPassword !== formData.value.confirmPassword) {
Service.Msg('两次输入的密码不一致!');
return;
}
Service.LoadIng('修改中...');
let data={
phone:formData.value.phone,
code:formData.value.code,
pwd:formData.value.newPassword,
npwd:formData.value.confirmPassword
}
LoginService.ForgetPwd(data).then(res=>{
if(res.code==0){
Service.LoadClose();
Service.Msg('密码修改成功!');
Service.GoPageDelse('/pages/login/login');
}else{
Service.Msg(res.msg)
}
})
};
const goToLogin = () => {
Service.GoPageBack();
};
</script>
<style scoped lang="scss">
.forgot-password-page {
min-height: 100vh;
background: linear-gradient(135deg, #FFF7ED 0%, #FFEDD5 100%);
display: flex;
align-items: center;
justify-content: center;
padding: 40rpx;
}
.forgot-password-container {
width: 100%;
background-color: #FFFFFF;
border-radius: 32rpx;
padding: 60rpx 40rpx;
box-shadow: 0 8rpx 32rpx rgba(249, 115, 22, 0.1);
}
.forgot-password-header {
text-align: center;
margin-bottom: 50rpx;
}
.logo {
width: 140rpx;
height: 140rpx;
border-radius: 50%;
margin-bottom: 24rpx;
}
.app-title {
display: block;
font-size: 44rpx;
font-weight: bold;
color: #1F2937;
margin-bottom: 10rpx;
}
.app-subtitle {
font-size: 28rpx;
color: #6B7280;
}
.forgot-password-form {
margin-bottom: 30rpx;
}
.input-item {
display: flex;
align-items: center;
background-color: #F9FAFB;
border-radius: 16rpx;
padding: 24rpx 30rpx;
margin-bottom: 28rpx;
border: 2rpx solid transparent;
transition: all 0.3s;
&:focus-within {
border-color: #F97316;
background-color: #FFF;
}
}
.input-field {
flex: 1;
margin-left: 20rpx;
font-size: 30rpx;
color: #1F2937;
}
.code-input {
flex: 1;
}
.code-btn {
margin-left: 20rpx;
padding: 16rpx 24rpx;
background: linear-gradient(135deg, #F97316, #FB923C);
color: #FFFFFF;
font-size: 26rpx;
border-radius: 12rpx;
border: none;
line-height: 1;
white-space: nowrap;
&:disabled {
opacity: 0.6;
background-color: #fff;
}
}
.submit-btn {
width: 100%;
padding: 0 30rpx;
background: linear-gradient(135deg, #F97316, #FB923C);
color: #FFFFFF;
font-size: 34rpx;
font-weight: 600;
border: none;
border-radius: 16rpx;
margin-top: 30rpx;
box-shadow: 0 8rpx 24rpx rgba(249, 115, 22, 0.3);
transition: all 0.3s;
&:active {
transform: scale(0.98);
box-shadow: 0 4rpx 12rpx rgba(249, 115, 22, 0.2);
}
}
.forgot-password-footer {
display: flex;
justify-content: center;
align-items: center;
margin-top: 40rpx;
}
.footer-text {
font-size: 28rpx;
color: #6B7280;
}
.footer-link {
font-size: 28rpx;
color: #F97316;
font-weight: 600;
margin-left: 8rpx;
}
</style>

336
src/pages/login/login.vue Normal file
View File

@@ -0,0 +1,336 @@
<template>
<view class="login-page">
<view class="login-container">
<view class="login-header">
<!-- <image :src="Service.GetIconImg('/static/logo/logo.png')" class="logo"></image> -->
<view class="app-title">欢迎登录</view>
</view>
<view class="login-tabs">
<view class="tab-item" :class="{ active: currentTab === 0 }" @click="switchTab(0)">
账户密码登录
</view>
<view class="tab-item" :class="{ active: currentTab === 1 }" @click="switchTab(1)">
手机号登录
</view>
</view>
<view class="login-form">
<view v-if="currentTab === 0" class="form-section">
<view class="input-item">
<u-icon name="account" size="20" color="#999"></u-icon>
<input v-model="passwordForm.username" class="input-field" type="text" placeholder="请输入手机号" />
</view>
<view class="input-item">
<u-icon name="lock" size="20" color="#999"></u-icon>
<input v-model="passwordForm.password" class="input-field"
:type="!showPassword?'password':'text'" placeholder="请输入密码" />
<u-icon :name="!showPassword ? 'eye-off' : 'eye'" size="20" color="#999"
@click="togglePassword"></u-icon>
</view>
</view>
<view v-if="currentTab === 1" class="form-section">
<view class="input-item">
<u-icon name="phone" size="20" color="#999"></u-icon>
<input v-model="phoneForm.phone" class="input-field" type="number" maxlength="11"
placeholder="请输入手机号" />
</view>
<view class="input-item">
<u-icon name="error-circle" size="20" color="#999"></u-icon>
<input v-model="phoneForm.code" class="input-field code-input" type="number" maxlength="6"
placeholder="请输入验证码" />
<button class="code-btn" :disabled="counting" @click="getCode">
{{ counting ? `${countdown}s` : '获取验证码' }}
</button>
</view>
</view>
<button class="login-btn" @click="handleLogin">
</button>
<view class="login-footer">
<text class="footer-text" @click="goToRegister">注册账户</text>
<text class="footer-text" @click="goToForgotPassword">忘记密码</text>
</view>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { Service } from '@/Service/Service';
import { LoginService } from "@/Service/api/LoginService"
import { onLoad } from '@dcloudio/uni-app';
const currentTab = ref(0);
const showPassword = ref(false);
const counting = ref(false);
const countdown = ref(60);
const passwordForm = ref({
username: '',
password: ''
});
const phoneForm = ref({
phone: '',
code: ''
});
onLoad((data:any) => {
console.log('onload',data);
})
const switchTab = (index : number) => {
currentTab.value = index;
};
const togglePassword = () => {
showPassword.value = !showPassword.value;
};
const getCode = () => {
if (!phoneForm.value.phone) {
Service.Msg('请输入手机号!');
return;
}
const phoneReg = /^1[3-9]\d{9}$/;
if (!phoneReg.test(phoneForm.value.phone)) {
Service.Msg('请输入正确的手机号!');
return;
}
counting.value = true;
LoginService.SendSms(phoneForm.value.phone, 'Login').then(res => {
if (res.code == 0) {
Service.Msg('验证码已发送!');
} else {
Service.Msg(res.msg)
}
})
const timer = setInterval(() => {
countdown.value--;
if (countdown.value <= 0) {
counting.value = false;
countdown.value = 60;
}
}, 1000);
};
const handleLogin = () => {
if (currentTab.value === 0) {
if (!passwordForm.value.username) {
Service.Msg('请输入手机号!');
return;
}
if (!passwordForm.value.password) {
Service.Msg('请输入密码!');
return;
}
Service.LoadIng('登录中...');
LoginService.Login(passwordForm.value.username, passwordForm.value.password).then(res => {
if (res.code == 0) {
Service.LoadClose();
Service.Msg('登录成功!');
Service.SetUserToken(res.data.token)
Service.GoPageTab('/pages/index/index')
} else {
Service.Msg(res.msg)
}
})
} else {
if (!phoneForm.value.phone) {
Service.Msg('请输入手机号!');
return;
}
if (!phoneForm.value.code) {
Service.Msg('请输入验证码!');
return;
}
Service.LoadIng('登录中...');
LoginService.PhoneLogin(phoneForm.value.phone, phoneForm.value.code).then(res => {
if (res.code == 0) {
Service.LoadClose();
Service.Msg('登录成功!');
// Service.SetUserToken(res.data.token)
let local = 'http://pt.pccsh.cn/#/pages/index/index';
window.location.href = "https://open.weixin.qq.com/connect/oauth2/authorize?appid=" + 'wxf892fef3811b3ded' + "&redirect_uri=" + local + "&response_type=code&scope=snsapi_userinfo&state=STATE#wechat_redirect";
} else {
Service.Msg(res.msg)
}
})
}
};
const goToRegister = () => {
Service.GoPage('/pages/login/register');
};
const goToForgotPassword = () => {
Service.GoPage('/pages/login/forgotPassword');
};
</script>
<style scoped lang="scss">
.login-page {
min-height: 100vh;
background: linear-gradient(135deg, #FFF7ED 0%, #FFEDD5 100%);
display: flex;
align-items: center;
justify-content: center;
padding: 40rpx;
}
.login-container {
width: 100%;
background-color: #FFFFFF;
border-radius: 32rpx;
padding: 60rpx 40rpx;
box-shadow: 0 8rpx 32rpx rgba(249, 115, 22, 0.1);
}
.login-header {
text-align: center;
margin-bottom: 60rpx;
}
.logo {
width: 160rpx;
height: 160rpx;
border-radius: 50%;
margin-bottom: 30rpx;
}
.app-title {
font-size: 48rpx;
font-weight: bold;
color: #1F2937;
}
.login-tabs {
display: flex;
margin-bottom: 50rpx;
border-bottom: 2rpx solid #F3F4F6;
}
.tab-item {
flex: 1;
text-align: center;
padding: 24rpx 0;
font-size: 30rpx;
color: #6B7280;
position: relative;
transition: all 0.3s;
}
.tab-item.active {
color: #F97316;
font-weight: 600;
}
.tab-item.active::after {
content: '';
position: absolute;
bottom: -2rpx;
left: 50%;
transform: translateX(-50%);
width: 80rpx;
height: 4rpx;
background: linear-gradient(90deg, #F97316, #FB923C);
border-radius: 2rpx;
}
.login-form {
.form-section {
margin-bottom: 40rpx;
}
}
.input-item {
display: flex;
align-items: center;
background-color: #F9FAFB;
border-radius: 16rpx;
padding: 24rpx 30rpx;
margin-bottom: 30rpx;
border: 2rpx solid transparent;
transition: all 0.3s;
&:focus-within {
border-color: #F97316;
background-color: #FFF;
}
}
.input-field {
flex: 1;
margin-left: 20rpx;
font-size: 30rpx;
color: #1F2937;
}
.code-input {
flex: 1;
}
.code-btn {
margin-left: 20rpx;
padding: 16rpx 24rpx;
background: linear-gradient(135deg, #F97316, #FB923C);
color: #FFFFFF;
font-size: 26rpx;
border-radius: 12rpx;
border: none;
line-height: 1;
white-space: nowrap;
&:disabled {
background: #D1D5DB;
color: #fff;
}
}
.login-btn {
width: 100%;
padding: 0 30rpx;
background: linear-gradient(135deg, #F97316, #FB923C);
color: #FFFFFF;
font-size: 34rpx;
font-weight: 600;
border: none;
border-radius: 16rpx;
margin-top: 40rpx;
box-shadow: 0 8rpx 24rpx rgba(249, 115, 22, 0.3);
transition: all 0.3s;
&:active {
transform: scale(0.98);
box-shadow: 0 4rpx 12rpx rgba(249, 115, 22, 0.2);
}
}
.login-footer {
display: flex;
justify-content: space-between;
margin-top: 40rpx;
}
.footer-text {
font-size: 26rpx;
color: #6B7280;
}
</style>

View File

@@ -0,0 +1,323 @@
<template>
<view class="register-page">
<view class="register-container">
<view class="register-header">
<!-- <image :src="Service.GetIconImg('/static/logo/logo.png')" class="logo"></image> -->
<text class="app-title">用户注册</text>
<text class="app-subtitle">创建您的账户</text>
</view>
<view class="register-form">
<view class="input-item">
<u-icon name="phone" size="20" color="#999"></u-icon>
<input v-model="registerForm.phone" class="input-field" type="number" maxlength="11"
placeholder="请输入手机号" />
</view>
<view class="input-item">
<u-icon name="error-circle" size="20" color="#999"></u-icon>
<input v-model="registerForm.code" class="input-field code-input" type="number" maxlength="6"
placeholder="请输入验证码" />
<button class="code-btn" :disabled="counting" @click="getCode">
{{ counting ? `${countdown}s` : '获取验证码' }}
</button>
</view>
<view class="input-item">
<u-icon name="lock" size="20" color="#999"></u-icon>
<input v-model="registerForm.password" class="input-field" placeholder="请设置密码" />
</view>
<view class="input-item">
<u-icon name="man-add" size="20" color="#999"></u-icon>
<input v-model="registerForm.id" :disabled='isInput' type="number" class="input-field" placeholder="推荐人手机号可为空" />
</view>
<view class="input-item">
<u-icon name="man-add" size="20" color="#999"></u-icon>
<input v-model="registerForm.payPwd" maxlength="6" type="number" class="input-field"
placeholder="请设置支付密码" />
</view>
<button class="register-btn" @click="handleRegister">
立即注册
</button>
<view class="register-footer">
<text class="footer-text">已有账号</text>
<text class="footer-link" @click="goToLogin">立即登录</text>
</view>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { Service } from '@/Service/Service';
import { LoginService } from "@/Service/api/LoginService"
import { onLoad } from '@dcloudio/uni-app';
const showPassword = ref(false);
const counting = ref(false);
const countdown = ref(60);
let visible = ref(false)
let isInput=ref(false)
const registerForm = ref({
phone: '',
code: '',
password: '',
id: '',
payPwd: ''
});
onLoad(() => {
registerForm.value.id = new URLSearchParams(location.hash.split('?')[1] || '').get('par') ?? '';
if(registerForm.value.id){
isInput.value=true
}else{
isInput.value=false
}
})
const togglePassword = () => {
showPassword.value = !showPassword.value;
};
const getCode = () => {
if (!registerForm.value.phone) {
Service.Msg('请输入手机号');
return;
}
const phoneReg = /^1[3-9]\d{9}$/;
if (!phoneReg.test(registerForm.value.phone)) {
Service.Msg('请输入正确的手机号');
return;
}
counting.value = true;
LoginService.SendSms(registerForm.value.phone, 'Register').then(res => {
if (res.code == 0) {
Service.Msg('验证码已发送!');
} else {
Service.Msg(res.msg)
}
})
const timer = setInterval(() => {
countdown.value--;
if (countdown.value <= 0) {
clearInterval(timer);
counting.value = false;
countdown.value = 60;
}
}, 1000);
};
const handleRegister = () => {
if (!registerForm.value.phone) {
Service.Msg('请输入手机号!');
return;
}
const phoneReg = /^1[3-9]\d{9}$/;
if (!phoneReg.test(registerForm.value.phone)) {
Service.Msg('请输入正确的手机号!');
return;
}
if (!registerForm.value.code) {
Service.Msg('请输入验证码!');
return;
}
if (!registerForm.value.password) {
Service.Msg('请设置密码!');
return;
}
if (!registerForm.value.payPwd) {
Service.Msg('请设置密码!');
return;
}
if (registerForm.value.password.length < 6) {
Service.Msg('密码长度不能少于6位!');
return;
}
if (registerForm.value.payPwd.length < 6) {
Service.Msg('支付密码长度不能少于6!');
return;
}
Service.LoadIng('注册中...');
let data = {
phone: registerForm.value.phone,
code: registerForm.value.code,
pwd: registerForm.value.password,
npwd: registerForm.value.password,
remPhone: registerForm.value.id,
payPwd: registerForm.value.payPwd
}
LoginService.Register(data).then(res => {
if (res.code == 0) {
Service.LoadClose();
Service.Msg('注册成功');
Service.SetUserToken(res.data.token)
Service.GoPageTab('/pages/index/index');
} else {
Service.Msg(res.msg)
}
})
};
const goToLogin = () => {
Service.GoPageDelse('/pages/login/login')
};
</script>
<style scoped lang="scss">
.register-page {
min-height: 100vh;
background: linear-gradient(135deg, #FFF7ED 0%, #FFEDD5 100%);
display: flex;
align-items: center;
justify-content: center;
padding: 40rpx;
}
.register-container {
width: 100%;
background-color: #FFFFFF;
border-radius: 32rpx;
padding: 60rpx 40rpx;
box-shadow: 0 8rpx 32rpx rgba(249, 115, 22, 0.1);
}
.register-header {
text-align: center;
margin-bottom: 50rpx;
}
.logo {
width: 140rpx;
height: 140rpx;
border-radius: 50%;
margin-bottom: 24rpx;
}
.app-title {
display: block;
font-size: 44rpx;
font-weight: bold;
color: #1F2937;
margin-bottom: 10rpx;
}
.app-subtitle {
font-size: 28rpx;
color: #6B7280;
}
.register-form {
margin-bottom: 30rpx;
}
.input-item {
display: flex;
align-items: center;
background-color: #F9FAFB;
border-radius: 16rpx;
padding: 24rpx 30rpx;
margin-bottom: 28rpx;
border: 2rpx solid transparent;
transition: all 0.3s;
&:focus-within {
border-color: #F97316;
background-color: #FFF;
}
}
.input-field {
flex: 1;
margin-left: 20rpx;
font-size: 30rpx;
color: #1F2937;
}
.area-display {
color: #1F2937;
}
.area-display:empty::before {
content: '请选择所在地区';
color: #9CA3AF;
}
.code-input {
flex: 1;
}
.code-btn {
margin-left: 20rpx;
padding: 16rpx 24rpx;
background: linear-gradient(135deg, #F97316, #FB923C);
color: #FFFFFF;
font-size: 26rpx;
border-radius: 12rpx;
border: none;
line-height: 1;
white-space: nowrap;
&:disabled {
opacity: 0.6;
background-color: #fff;
}
}
.register-btn {
width: 100%;
padding: 0 30rpx;
background: linear-gradient(135deg, #F97316, #FB923C);
color: #FFFFFF;
font-size: 34rpx;
font-weight: 600;
border: none;
border-radius: 16rpx;
margin-top: 30rpx;
box-shadow: 0 8rpx 24rpx rgba(249, 115, 22, 0.3);
transition: all 0.3s;
&:active {
transform: scale(0.98);
box-shadow: 0 4rpx 12rpx rgba(249, 115, 22, 0.2);
}
}
.register-footer {
display: flex;
justify-content: center;
align-items: center;
margin-top: 40rpx;
}
.footer-text {
font-size: 28rpx;
color: #6B7280;
}
.footer-link {
font-size: 28rpx;
color: #F97316;
font-weight: 600;
margin-left: 8rpx;
}
</style>

View File

@@ -0,0 +1,361 @@
<template>
<view class="group-goods-page">
<!-- 页面顶部 -->
<view class="page-header">
<image :src="Service.GetIconImg('/static/goods/pingtuan.png')" mode="widthFix" style="width: 100%; border-radius: 20rpx;" alt="" />
</view>
<!-- 拼单商品列表 -->
<view class="goods-waterfall" v-if="goodsList.length > 0">
<view class="waterfall-col">
<view class="goods-card" v-for="(item, index) in leftList" :key="index" @click="goGroupDetail(item)">
<view class="goods-img-wrap">
<image class="goods-img" :src="item.goodsImg" mode="widthFix"></image>
<view class="group-badge">拼团</view>
</view>
<view class="goods-info">
<view class="goods-name">{{ item.goodsName }}</view>
<view class="goods-price-row">
<view class="price-wrap">
<text class="group-price">¥<text class="price-num">{{ item.price }}</text></text>
<text class="original-price">¥{{ item.maxPrice }}</text>
</view>
<view class="sold-count">已售 {{ item.sale }}</view>
</view>
<view class="go-group-btn" @click.stop="goGroupDetail(item)">
<text class="btn-text">去拼单</text>
</view>
</view>
</view>
</view>
<view class="waterfall-col">
<view class="goods-card" v-for="(item, index) in rightList" :key="index" @click="goGroupDetail(item)">
<view class="goods-img-wrap">
<image class="goods-img" :src="item.goodsImg" mode="widthFix"></image>
<view class="group-badge">拼团</view>
</view>
<view class="goods-info">
<view class="goods-name">{{ item.goodsName }}</view>
<view class="goods-price-row">
<view class="price-wrap">
<text class="group-price">¥<text class="price-num">{{ item.price }}</text></text>
<text class="original-price">¥{{ item.maxPrice }}</text>
</view>
<view class="sold-count">已售 {{ item.sale }}</view>
</view>
<view class="go-group-btn" @click.stop="goGroupDetail(item)">
<text class="btn-text">去拼单</text>
</view>
</view>
</view>
</view>
</view>
<!-- 空状态 -->
<view v-else class="empty-state">
<image :src="Service.GetpayImg('/static/userFunc/null.png')" class="empty-image" mode="aspectFit"></image>
<text class="empty-text">暂无拼单商品</text>
<text class="empty-desc">稍后再来看看吧</text>
</view>
</view>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue';
import { onLoad } from '@dcloudio/uni-app';
import { Service } from '@/Service/Service';
import { GoodsService } from '@/Service/api/GoodsService'
// 商品列表
const goodsList = ref<Array<any>>([]);
// 左右两列
const leftList = computed(() => {
return goodsList.value.filter((_, i) => i % 2 === 0);
});
const rightList = computed(() => {
return goodsList.value.filter((_, i) => i % 2 === 1);
});
let page = ref(1) // 当前页码
let status = ref('nomore');
onLoad(() => {
uni.setNavigationBarTitle({
title: '拼单商品'
});
getData()
});
const getData = () => {
goodsList.value = []
page.value = 1
status.value = 'loadmore'
getList()
}
/**
* 加载商品列表数据
* 包含轮播图、优选商品和商品列表
*/
const getList = () => {
// 防止重复加载
if (status.value == 'loading' || status.value == 'nomore') {
return
}
status.value = 'loading'
// 调用API获取商品数据
GoodsService.GetTuanGoodsList( page.value).then(res => {
if (res.code == 0) {
goodsList.value=[...goodsList.value,...res.data.list]
// 更新加载状态
status.value = res.data.goodsList.length == 10 ? 'loadmore' : 'nomore'
page.value++
} else {
Service.Msg(res.msg)
}
})
}
// 查看拼单详情
const goGroupDetail = (item : any) => {
// TODO: 替换为实际的拼单商品详情路径
Service.GoPage('/pages/webView/goodsDetail?goodsId=' + item.goodsId);
};
</script>
<style scoped lang="scss">
page {
background-color: #f5f5f5;
}
.group-goods-page {
min-height: 100vh;
padding: 20rpx;
box-sizing: border-box;
}
/* 页面顶部 */
.page-header {
margin-bottom: 20rpx;
}
.header-title {
background: linear-gradient(135deg, #FF6A00, #FF7F00);
border-radius: 20rpx;
padding: 40rpx 30rpx;
display: flex;
flex-direction: column;
gap: 10rpx;
box-shadow: 0 8rpx 24rpx rgba(255, 106, 0, 0.2);
}
.page-title {
font-size: 40rpx;
color: #fff;
font-weight: bold;
}
.page-desc {
font-size: 26rpx;
color: rgba(255, 255, 255, 0.9);
}
/* 瀑布流 */
.goods-waterfall {
display: flex;
justify-content: space-between;
}
.waterfall-col {
width: 48.5%;
display: flex;
flex-direction: column;
gap: 20rpx;
}
.goods-card {
background: #fff;
border-radius: 20rpx;
overflow: hidden;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.04);
transition: transform 0.2s, box-shadow 0.2s;
}
.goods-card:active {
transform: scale(0.98);
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.08);
}
.goods-img-wrap {
position: relative;
width: 100%;
overflow: hidden;
background-color: #f5f5f5;
}
.goods-img {
width: 100%;
display: block;
}
.group-badge {
position: absolute;
top: 0;
left: 0;
background: linear-gradient(135deg, #FF6A00, #FF4500);
color: #fff;
font-size: 20rpx;
font-weight: 600;
padding: 6rpx 14rpx;
border-radius: 0 0 16rpx 0;
z-index: 1;
}
.goods-info {
padding: 18rpx;
}
.goods-name {
font-size: 26rpx;
color: #333;
line-height: 1.5;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
text-overflow: ellipsis;
font-weight: 500;
}
.goods-tags {
display: flex;
flex-wrap: wrap;
margin-top: 10rpx;
gap: 8rpx;
}
.tag-text {
font-size: 20rpx;
color: #FF6A00;
border: 1rpx solid rgba(255, 106, 0, 0.3);
padding: 2rpx 10rpx;
border-radius: 8rpx;
background: rgba(255, 106, 0, 0.05);
}
/* 拼团进度 */
.group-progress {
margin-top: 14rpx;
}
.progress-bar {
width: 100%;
height: 12rpx;
background: #f0f0f0;
border-radius: 6rpx;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: linear-gradient(to right, #FF3D3D, #FF6A00);
border-radius: 6rpx;
transition: width 0.5s ease;
}
.progress-text {
font-size: 20rpx;
color: #999;
margin-top: 6rpx;
display: block;
}
/* 价格 */
.goods-price-row {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 14rpx;
}
.price-wrap {
display: flex;
align-items: baseline;
gap: 8rpx;
}
.group-price {
font-size: 24rpx;
color: #FF3D3D;
font-weight: bold;
}
.price-num {
font-size: 34rpx;
}
.original-price {
font-size: 22rpx;
color: #bbb;
text-decoration: line-through;
}
.sold-count {
font-size: 20rpx;
color: #999;
}
/* 去拼单按钮 */
.go-group-btn {
margin-top: 16rpx;
height: 64rpx;
border-radius: 32rpx;
background: linear-gradient(to right, #FF6A00, #FF7F00);
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4rpx 16rpx rgba(255, 106, 0, 0.25);
}
.btn-text {
font-size: 26rpx;
color: #fff;
font-weight: 600;
}
/* 空状态 */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 160rpx 40rpx;
}
.empty-image {
width: 260rpx;
height: 260rpx;
margin-bottom: 24rpx;
opacity: 0.6;
}
.empty-text {
font-size: 28rpx;
color: #999;
font-weight: 500;
margin-bottom: 12rpx;
}
.empty-desc {
font-size: 24rpx;
color: #bbb;
}
</style>

View File

@@ -0,0 +1,339 @@
<template>
<view class="add-address-page">
<view class="page-header">
<text class="page-title">{{ !addressId ? '新增地址' : '修改地址' }}</text>
<text class="page-desc">请准确填写您的收货信息</text>
</view>
<view class="form-card">
<up-form labelPosition="top" :model="userData" labelWidth='200rpx' ref="form1">
<up-form-item label="收货人" prop="userInfo.name" ref="item1">
<view class="input-wrap">
<up-icon name="account" color="#FF6A00" size="18"></up-icon>
<up-input v-model="userData.name" placeholder="请输入收货人姓名" border="none"
:customStyle="{ flex: 1, paddingLeft: '12rpx' }"></up-input>
</view>
</up-form-item>
<up-form-item label="手机号" prop="userInfo.name" ref="item1">
<view class="input-wrap">
<up-icon name="phone" color="#FF6A00" size="18"></up-icon>
<up-input v-model="userData.phone" placeholder="请输入手机号码" border="none"
:customStyle="{ flex: 1, paddingLeft: '12rpx' }" type="number" maxlength="11"></up-input>
</view>
</up-form-item>
<up-form-item label="所在地区" prop="userInfo.name" ref="item1">
<view class="region-picker" @click="visible=true">
<view class="region-left">
<up-icon name="map" color="#FF6A00" size="18"></up-icon>
<text v-if="!userData.procince" class="region-placeholder">请选择省//</text>
<text v-else class="region-value">{{ userData.procince }} {{ userData.city }} {{ userData.region }}</text>
</view>
<up-icon name="arrow-right" color="#C0C4CC" size="16"></up-icon>
</view>
</up-form-item>
<up-form-item label="详细地址" prop="userInfo.name" ref="item1">
<view class="textarea-wrap">
<up-textarea v-model="userData.detail" placeholder="请输入街道、楼栋、门牌号等详细地址" border="none"
:customStyle="{ flex: 1, paddingLeft: '12rpx', backgroundColor: 'transparent' }"></up-textarea>
</view>
</up-form-item>
</up-form>
</view>
<view class="switch-card">
<view class="switch-row">
<view class="switch-left">
<view class="switch-icon-wrap">
<up-icon name="checkmark-circle" color="#FF6A00" size="18"></up-icon>
</view>
<view class="switch-text-wrap">
<text class="switch-label">设为默认地址</text>
<text class="switch-desc">下次下单时会默认使用此地址</text>
</view>
</view>
<up-switch v-model="userData.switchValue" size='20' activeColor='var(--nav-mian)' @change="switchChange">
</up-switch>
</view>
</view>
<view class="bottom-bar">
<up-button @click="save()" color='var(--nav-mian)' shape='circle'
:customStyle="{ width: '90%', margin: '0 auto', height: '88rpx', fontSize: '32rpx', fontWeight: '600' }"
:text="!addressId?'保存地址':'修改地址'"></up-button>
</view>
<piaoyi-cityPicker column="3" default-value="411002" :mask-close-able="true" @confirm="areaConfirm"
@cancel="areaCancel" :visible="visible" />
</view>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { Service } from "@/Service/Service"
import { AddressService } from "@/Service/api/AddressService"
import { onLoad } from '@dcloudio/uni-app';
let visible = ref(false)
let userData = ref({
name: '',
detail: '',
switchValue: false,
phone: '',
procince: '',
city: '',
region: '',
})
let addressId = ref('')
onLoad((data: any) => {
if (data.addressId) {
addressId.value = data.addressId
getData()
}
})
const getData = () => {
AddressService.GetUserAddressInfo(addressId.value).then(res => {
if (res.code == 0) {
userData.value.name = res.data.addressInfo.realName
userData.value.phone = res.data.addressInfo.phone
userData.value.procince = res.data.addressInfo.province
userData.value.city = res.data.addressInfo.city
userData.value.region = res.data.addressInfo.region
userData.value.detail = res.data.addressInfo.address
userData.value.switchValue = res.data.addressInfo.isDefault === 1 ? true : false
} else {
Service.Msg(res.msg)
}
})
}
const save = () => {
if (!userData.value.name) {
Service.Msg('请填写姓名!')
return
}
if (!userData.value.phone) {
Service.Msg('请填写手机号!')
return
}
if (!userData.value.procince) {
Service.Msg('请选择地址!')
return
}
if (!userData.value.detail) {
Service.Msg('请输入详细地址!')
return
}
let data = {
addressId: addressId.value,
name: userData.value.name,
phone: userData.value.phone,
province: userData.value.procince,
city: userData.value.city,
region: userData.value.region,
address: userData.value.detail,
isDefault: userData.value.switchValue ? 1 : 0
}
AddressService.AddUserAddress(data).then(res => {
if (res.code == 0) {
Service.Msg(!addressId.value ? '添加成功!' : '修改成功!')
setTimeout(() => {
Service.GoPageBack()
}, 1000)
} else {
Service.Msg(res.msg)
}
})
}
const switchChange = (e: any) => {
userData.value.switchValue = e
}
const areaConfirm = (e: any) => {
visible.value = false
userData.value.procince = e.provinceName
userData.value.city = e.cityName
userData.value.region = e.areaName
}
const areaCancel = (e: any) => {
visible.value = false
}
</script>
<style lang="scss">
page {
background-color: #F8F9FA;
}
.add-address-page {
display: flex;
flex-direction: column;
padding-bottom: 160rpx;
}
/* ===== 页面头部 ===== */
.page-header {
padding: 40rpx 30rpx 30rpx;
}
.page-title {
font-size: 40rpx;
font-weight: 700;
color: #1F2937;
display: block;
}
.page-desc {
font-size: 24rpx;
color: #9CA3AF;
margin-top: 10rpx;
display: block;
}
/* ===== 表单卡片 ===== */
.form-card {
background: #fff;
margin: 0 30rpx;
border-radius: 28rpx;
padding: 24rpx 24rpx 10rpx;
box-shadow: 0 4rpx 24rpx rgba(0, 0, 0, 0.04);
}
/* ===== 输入框 ===== */
.input-wrap {
width: 100%;
display: flex;
align-items: center;
background-color: #F8F9FA;
border-radius: 16rpx;
padding: 20rpx 24rpx;
margin-top: 12rpx;
border: 2rpx solid transparent;
transition: all 0.2s;
}
.input-wrap:focus-within {
border-color: var(--nav-mian);
background-color: #fff;
}
.region-picker {
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
background-color: #F8F9FA;
border-radius: 16rpx;
padding: 24rpx;
margin-top: 12rpx;
border: 2rpx solid transparent;
transition: all 0.2s;
}
.region-picker:active {
background-color: #F0F0F0;
}
.region-left {
display: flex;
align-items: center;
gap: 12rpx;
flex: 1;
}
.region-placeholder {
color: #C0C4CC;
font-size: 28rpx;
}
.region-value {
color: #1F2937;
font-size: 28rpx;
font-weight: 500;
}
.textarea-wrap {
width: 100%;
display: flex;
align-items: flex-start;
background-color: #F8F9FA;
border-radius: 16rpx;
padding: 0rpx 24rpx;
margin-top: 12rpx;
border: 2rpx solid transparent;
transition: all 0.2s;
}
.textarea-wrap:focus-within {
border-color: var(--nav-mian);
background-color: #fff;
}
/* ===== 默认地址开关 ===== */
.switch-card {
background: #fff;
margin: 24rpx 30rpx 0;
border-radius: 28rpx;
padding: 24rpx 30rpx;
box-shadow: 0 4rpx 24rpx rgba(0, 0, 0, 0.04);
}
.switch-row {
display: flex;
align-items: center;
justify-content: space-between;
}
.switch-left {
display: flex;
align-items: center;
gap: 16rpx;
}
.switch-icon-wrap {
width: 60rpx;
height: 60rpx;
border-radius: 16rpx;
background: linear-gradient(135deg, #FFF7ED, #FFEDD5);
display: flex;
align-items: center;
justify-content: center;
}
.switch-label {
font-size: 30rpx;
font-weight: 600;
color: #1F2937;
display: block;
}
.switch-desc {
font-size: 22rpx;
color: #9CA3AF;
margin-top: 4rpx;
display: block;
}
/* ===== 底部按钮 ===== */
.bottom-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
padding: 20rpx 0;
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
background: #fff;
box-shadow: 0 -4rpx 24rpx rgba(0, 0, 0, 0.04);
display: flex;
align-items: center;
justify-content: center;
z-index: 99;
}
</style>

View File

@@ -0,0 +1,221 @@
<template>
<view class="address-manager">
<!-- 地址列表 -->
<view class="address-list">
<!-- 地址项1 - 默认地址 -->
<view v-for="(item,index) in addresses" :key="index" class="address-item">
<view class="address-content">
<view class="name-phone">
<text class="name">{{item.realName }}</text>
<text class="phone">{{phone(item.phone) }}</text>
<view v-if="item.isDefault==1" class="default-tag">默认</view>
</view>
<text class="address">{{ item.province+item.city+item.region+item.address }}</text>
</view>
<view class="" style="display: flex;align-items: center; gap: 15rpx; ">
<up-icon @click="Service.GoPage('/pages/userFunc/addAddress?addressId='+item.addressId)"
name="edit-pen" size="20"></up-icon>
<up-icon @click="dele(item.addressId)" name="trash" size="20"></up-icon>
</view>
</view>
</view>
<up-loadmore :status="status" />
<view class="" style="width: 100%; height: 120rpx;">
</view>
<!-- 新增地址按钮 -->
<view class=""
style=" background-color: #fff; border-top: 1rpx solid #e2e2e2; position: fixed; left: 0; bottom: 0; width: 100vw; height: 110rpx; ">
<up-button @click="Service.GoPage('/pages/userFunc/addAddress')" color='var(--nav-mian)' shape='circle'
style="width: 90%; margin: 15rpx auto 0; " text="新增地址"></up-button>
</view>
</view>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { Service } from "@/Service/Service"
import { onShow, onLoad, onReachBottom } from "@dcloudio/uni-app";
import { AddressService } from "@/Service/api/AddressService"
// 地址数据
const addresses = ref<Array<any>>([
]);
let page = ref(1)
let status = ref('nomore')
onLoad(() => {
});
onShow(() => {
getData()
});
onReachBottom(() => {
getList()
})
const getData = () => {
page.value = 1
status.value = 'loadmore'
addresses.value = []
getList()
}
const getList = () => {
if (status.value == 'loading' || status.value == 'nomore') {
return
}
status.value = 'loading'
AddressService.GetUserAddressList(page.value).then(res => {
if (res.code == 0) {
addresses.value = [...addresses.value, ...res.data.list]
status.value = res.data.list.length == 10 ? 'loadmore' : 'nomore'
page.value++
} else {
Service.Msg(res.msg)
}
})
}
const phone = (data : any) => {
return String(data).slice(0, 3) + '****' + String(data).slice(-4)
}
const dele = (id : any) => {
uni.showModal({
title: '提示', // 对话框标题
content: '是否删除该地址', // 显示的内容
success: function (res) {
if (res.confirm) {
AddressService.DelUserAddress(id).then(res => {
if (res.code == 0) {
Service.Msg('删除成功!')
} else {
Service.Msg(res.msg)
}
})
} else if (res.cancel) {
console.log('用户点击取消');
}
}
});
}
</script>
<style scoped lang="scss">
page {
background-color: #f5f5f5;
}
.address-manager {
padding: 20rpx;
}
/* 提示信息样式 */
.tip-bar {
background-color: #fff8e6;
padding: 20rpx 30rpx;
display: flex;
align-items: center;
.info-icon {
color: #ff9c07;
margin-right: 10rpx;
}
.tip-text {
font-size: 26rpx;
color: #ff9c07;
flex: 1;
}
}
/* 地址列表样式 */
.address-list {
.address-item {
border-radius: 15rpx;
padding: 30rpx;
background-color: #fff;
margin-top: 20rpx;
display: flex;
justify-content: space-between;
align-items: flex-start;
border-bottom: 1rpx solid #f5f5f5;
&:last-child {
border-bottom: none;
}
.address-content {
flex: 1;
.name-phone {
display: flex;
align-items: center;
margin-bottom: 10rpx;
.name {
font-size: 28rpx;
color: #333;
font-weight: 500;
margin-right: 20rpx;
}
.phone {
font-size: 26rpx;
color: #666;
margin-right: 15rpx;
}
.default-tag {
background-color: #ff6b00;
color: #fff;
font-size: 20rpx;
padding: 2rpx 15rpx;
border-radius: 20rpx;
}
}
.address {
font-size: 26rpx;
color: #666;
line-height: 1.5;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
margin-top: 20rpx;
}
}
.edit-icon {
width: 30rpx;
height: 30rpx;
color: #999;
margin-top: 5rpx;
}
}
}
/* 新增地址按钮样式 */
.add-btn {
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: 100rpx;
background-color: #ff6b00;
color: #fff;
font-size: 32rpx;
font-weight: 500;
display: flex;
align-items: center;
justify-content: center;
border-radius: 0;
}
</style>

View File

@@ -0,0 +1,257 @@
<template>
<view class="page">
<view class="content">
<view v-if="productFavorites.length > 0" class="goods-waterfall">
<view class="waterfall-col">
<view class="goods-card" v-for="(item, index) in leftList" :key="index"
@click="Service.GoPage('/pages/webView/goodsDetail?goodsId='+item.goodsId)">
<view class="goods-img-wrap">
<image :src="Service.GetMateUrlByImg(item.goodsImg)" mode="widthFix" class="goods-img"></image>
<view class="favorite-icon-wrap" @click.stop="cancleCollect(item.goodsId)">
<up-icon name="heart-fill" color="#FF6A00" size="16"></up-icon>
</view>
</view>
<view class="goods-info">
<text class="goods-name">{{ item.goodsName }}</text>
<view class="goods-price-row">
<text class="goods-price">¥<text class="price-num">{{ item.price }}</text></text>
<text class="goods-old">¥{{ item.maxPrice }}</text>
</view>
</view>
</view>
</view>
<view class="waterfall-col">
<view class="goods-card" v-for="(item, index) in rightList" :key="index"
@click="Service.GoPage('/pages/webView/goodsDetail?goodsId='+item.goodsId)">
<view class="goods-img-wrap">
<image :src="Service.GetMateUrlByImg(item.goodsImg)" mode="widthFix" class="goods-img"></image>
<view class="favorite-icon-wrap" @click.stop="cancleCollect(item.goodsId)">
<up-icon name="heart-fill" color="#FF6A00" size="16"></up-icon>
</view>
</view>
<view class="goods-info">
<text class="goods-name">{{ item.goodsName }}</text>
<view class="goods-price-row">
<text class="goods-price">¥<text class="price-num">{{ item.price }}</text></text>
<text class="goods-old">¥{{ item.maxPrice }}</text>
</view>
</view>
</view>
</view>
</view>
<view v-else class="empty-state">
<image :src="Service.GetpayImg('/static/userFunc/null.png')" class="empty-image"></image>
<text class="empty-text">暂无收藏商品</text>
</view>
<up-loadmore v-if="productFavorites.length > 0" style="margin-top: 12rpx;" :status="status" />
</view>
</view>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue';
import { Service } from "@/Service/Service"
import { onLoad, onReachBottom } from '@dcloudio/uni-app';
import { CollectService } from "@/Service/api/CollectService"
let status = ref('nomore')
let page = ref(1)
let productFavorites = ref<Array<any>>([])
let leftList = computed(() => {
return productFavorites.value.filter((_, i) => i % 2 === 0)
})
let rightList = computed(() => {
return productFavorites.value.filter((_, i) => i % 2 === 1)
})
onLoad(() => {
getData()
})
onReachBottom(() => {
getList()
})
const getData = () => {
productFavorites.value = []
page.value = 1
status.value = 'loadmore'
getList()
}
const getList = () => {
if (status.value == 'loading' || status.value == 'nomore') {
return
}
status.value = 'loading'
CollectService.GetUserCollectList(page.value).then(res => {
if (res.code == 0) {
productFavorites.value = [...productFavorites.value, ...res.data.list]
status.value = res.data.list.length == 10 ? 'loadmore' : 'nomore'
page.value++
} else {
Service.Msg(res.msg)
}
})
}
const cancleCollect = (id: any) => {
CollectService.AddUserCollect({ goodsId: id }).then(res => {
if (res.code == 0) {
Service.Msg('已取消收藏')
getData()
} else {
Service.Msg(res.msg)
}
})
}
</script>
<style scoped lang="scss">
.page {
padding: 20rpx 0;
min-height: 100vh;
background-color: #F8F9FA;
}
.page-header {
padding: 30rpx 30rpx 20rpx;
display: flex;
align-items: baseline;
gap: 16rpx;
}
.page-title {
font-size: 30rpx;
font-weight: 700;
color: #1F2937;
}
.page-count {
font-size: 24rpx;
color: #9CA3AF;
font-weight: 500;
}
.content {
padding: 0 24rpx 30rpx;
}
/* ===== 瀑布流 ===== */
.goods-waterfall {
display: flex;
gap: 16rpx;
}
.waterfall-col {
width: 50%;
display: flex;
flex-direction: column;
gap: 16rpx;
}
.goods-card {
background: #fff;
border-radius: 20rpx;
overflow: hidden;
box-shadow: 0 2rpx 16rpx rgba(0, 0, 0, 0.04);
transition: transform 0.2s, box-shadow 0.2s;
}
.goods-card:active {
transform: scale(0.97);
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.06);
}
.goods-img-wrap {
position: relative;
width: 100%;
overflow: hidden;
}
.goods-img {
width: 100%;
display: block;
background-color: #f5f5f5;
}
.favorite-icon-wrap {
position: absolute;
top: 12rpx;
right: 12rpx;
width: 48rpx;
height: 48rpx;
border-radius: 50%;
background: rgba(255, 255, 255, 0.9);
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
z-index: 1;
}
.goods-info {
padding: 14rpx 16rpx 18rpx;
}
.goods-name {
font-size: 26rpx;
color: #1F2937;
line-height: 1.5;
font-weight: 500;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
text-overflow: ellipsis;
}
.goods-price-row {
display: flex;
align-items: center;
margin-top: 10rpx;
gap: 10rpx;
}
.goods-price {
font-weight: 700;
font-size: 28rpx;
color: #FF6A00;
}
.price-num {
font-size: 32rpx;
}
.goods-old {
font-size: 22rpx;
color: #C0C4CC;
text-decoration: line-through;
}
/* ===== 空状态 ===== */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
padding: 160rpx 20rpx;
}
.empty-image {
width: 280rpx;
height: 280rpx;
margin-bottom: 24rpx;
opacity: 0.6;
}
.empty-text {
font-size: 28rpx;
color: #C0C4CC;
font-weight: 500;
}
</style>

View File

@@ -0,0 +1,322 @@
<template>
<view class="page">
<!-- 评价分类标签 -->
<view class="evaluation-tabs">
<up-tabs :list="tabList" :current="currentTab" @click="handleTabChange" :scrollable='false'
lineColor='var(--nav-mian)' lineWidth='50'
:activeStyle="{color: 'var(--nav-mian)',fontWeight: 'bold',transform: 'scale(1.05)'}"
:inactiveStyle="{color: '#606266',transform: 'scale(1)'}"
itemStyle=" padding-left: 30rpx; padding-right: 30rpx; height: 34px;"></up-tabs>
</view>
<!-- 评价列表 -->
<view class="evaluation-list">
<view class="evaluation-item" v-for="(item, index) in evaluations" :key="index">
<!-- 商品信息 -->
<view class="" style=" display: flex;align-items: start;">
<view class="goods-info">
<image :src="Service.GetMateUrlByImg(item.goodsImg)" mode="aspectFill" class="goods-image">
</image>
</view>
<!-- 评价内容 -->
<view class="evaluation-content">
<view class="" style="display: flex; align-items: start;">
<view class="goods-details">
<text class="goods-name">{{ item.goodsName }}</text>
</view>
<u-icon name="trash" size="40rpx" color="#CCCCCC" class="delete-icon"
@click="deleteEvaluation(item)"></u-icon>
</view>
<!-- 星级评分 -->
<view class="" style="display: flex; align-items: baseline; gap: 10rpx;">
<up-rate v-model="item.source" style="margin-top: 15rpx;" gutter='2' size="28rpx"
:allowHalf='true' active-color="#FF8C00" :readonly="true"></up-rate>
<view class="" style="color:#FF8C00 ; font-size: 24rpx; font-weight: bold;">
{{Number(item.source).toFixed(1) }}
</view>
</view>
</view>
</view>
<view class="">
<!-- 评价文字 -->
<view class="" style="padding: 20rpx; background-color: #f1f1f1; border-radius: 20rpx;">
<text class="evaluation-text">{{ item.remark }}</text>
<!-- 评价图片 -->
<view class="evaluation-images" v-if="item.imgs">
<image :src="Service.GetMateUrlByImg(img)" mode="aspectFill" class="evaluation-img"
v-for="(img, imgIndex) in JSON.parse(item.imgs)" :key="imgIndex"></image>
</view>
</view>
<!-- 评价信息 -->
<view class="evaluation-meta" >
<view class="">
</view>
<text class="evaluation-date">{{ Service.formatDate(item.addTime, 1) }}</text>
</view>
</view>
</view>
</view>
<!-- 空状态 -->
<view class="empty-state" v-if="evaluations.length === 0">
<image :src="Service.GetpayImg('/static/userFunc/null.png')" />
<text class="empty-text">暂无评价</text>
</view>
<up-loadmore v-if="evaluations.length !== 0" :status="status" />
<view class="" style="width: 100%; height: 80rpx;">
</view>
</view>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { Service } from '@/Service/Service'
import { UserService } from "@/Service/api/UserService"
import { onLoad, onReachBottom } from '@dcloudio/uni-app';
// 评价分类标签
const tabList = ref([
{ name: '全部' },
{ name: '好评' },
{ name: '中评' },
{ name: '差评' }
]);
// 当前选中的标签
const currentTab = ref(0);
let page = ref(1);
let status = ref('nomore');
let viewCount = ref(0);
// 评价列表数据
const evaluations = ref<Array<any>>([]);
onLoad(() => {
getData();
});
onReachBottom(() => {
getList();
});
// 处理标签切换
const handleTabChange = (index : any) => {
currentTab.value = index.index;
getData();
};
const getData = () => {
evaluations.value = [];
page.value = 1;
status.value = 'loadmore';
getList();
};
const getList = () => {
if (status.value == 'loading' || status.value == 'nomore') {
return;
}
status.value = 'loading';
let source = 0;
if (currentTab.value == 1) source = 1;
else if (currentTab.value == 2) source = 2;
else if (currentTab.value == 3) source = 3;
UserService.GetUserEvaluateList(source, page.value).then(res => {
if (res.code == 0) {
evaluations.value = [...evaluations.value, ...res.data.list];
if (res.data.viewCount) {
viewCount.value = res.data.viewCount;
}
status.value = res.data.list.length == 10 ? 'loadmore' : 'nomore';
page.value++;
} else {
Service.Msg(res.msg);
}
});
};
// 删除评价
const deleteEvaluation = (item : any) => {
uni.showModal({
title: '确认删除',
content: '确定要删除这条评价吗?',
success: (res) => {
if (res.confirm) {
UserService.DelUserEvaluate(item.geId).then(res=>{
if (res.code == 0) {
Service.Msg(res.data)
getData()
} else {
Service.Msg(res.msg);
}
})
}
}
});
};
</script>
<style scoped lang="scss">
.page {
background-color: #f5f5f5;
min-height: 100vh;
}
/* 浏览统计样式 */
.view-stats {
padding: 20rpx 30rpx;
display: flex;
align-items: end;
background-color: #fff;
}
.stats-text {
font-size: 26rpx;
color: #4B5563;
margin-left: 10rpx;
}
/* 评价分类标签样式 */
.evaluation-tabs {
background-color: #ffffff;
margin-bottom: 20rpx;
}
/* 评价列表样式 */
.evaluation-list {
padding: 0 30rpx 20rpx;
}
.evaluation-item {
background-color: #ffffff;
border-radius: 16rpx;
padding: 30rpx;
margin-bottom: 20rpx;
}
/* 商品信息样式 */
.goods-info {
display: flex;
align-items: center;
margin-bottom: 24rpx;
}
.goods-image {
width: 140rpx;
height: 140rpx;
border-radius: 12rpx;
margin-right: 24rpx;
}
.goods-details {
flex: 1;
}
.goods-name {
font-size: 32rpx;
color: #333333;
margin-bottom: 10rpx;
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
overflow: hidden;
}
.goods-price {
font-size: 28rpx;
color: #6B7280;
font-weight: 600;
}
.delete-icon {
margin-left: 20rpx;
}
/* 评价内容样式 */
.evaluation-content {
width: 100%;
}
.evaluation-text {
font-size: 26rpx;
color: #333333;
line-height: 48rpx;
display: block;
white-space: pre-wrap;
}
/* 评价图片样式 */
.evaluation-images {
display: flex;
flex-wrap: wrap;
}
.evaluation-img {
width: 160rpx;
height: 160rpx;
border-radius: 12rpx;
margin-right: 16rpx;
margin-bottom: 16rpx;
}
/* 评价信息样式 */
.evaluation-meta {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 15rpx;
}
.evaluation-date {
font-size: 26rpx;
color: #999999;
}
.evaluation-actions {
display: flex;
align-items: center;
}
.hot-tag {
font-size: 26rpx;
color: #FF6600;
margin-right: 24rpx;
display: flex;
align-items: center;
}
.edit-btn {
font-size: 26rpx;
color: #FF6600;
}
/* 空状态样式 */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding-top: 120rpx;
img {
width: 200rpx;
height: 200rpx;
}
.empty-text {
font-size: 28rpx;
color: #999;
margin-top: 30rpx;
}
}
</style>

View File

@@ -0,0 +1,359 @@
<template>
<view class="review-page">
<!-- 商品信息 -->
<view v-for="(item,index) in orderDetail" :key="index" class="product-info">
<image :src="Service.GetMateUrlByImg(item.goodsImg)" class="product-image"></image>
<view class="product-details">
<text class="product-name">{{ item.goodsName }}</text>
<view class="" style=" display: flex; align-items: baseline; gap: 12rpx;">
规格:
<text v-for="(ruleItem,ruleIndex) in item.rule.split('-')" :key="ruleIndex"
class="product-attributes">{{ruleItem }}</text>
</view>
</view>
</view>
<!-- 评分区域 -->
<view class="rating-section">
<view class="rating-item">
<text class="rating-label">评价星级</text>
<view class="" style="display: flex; align-items: end;">
<up-rate count="5" v-model="source" activeColor='#F97316'></up-rate>
<text style="color: #F97316; font-weight: 600; margin-left: 15rpx;">{{source.toFixed(1)}}</text>
</view>
</view>
</view>
<!-- 评价心得 -->
<view class="review-content-section">
<text class="section-title">评价心得</text>
<up-textarea v-model="reviewContent" placeholder="请输入内容" count></up-textarea>
</view>
<!-- 上传图片 -->
<view class="upload-section">
<text class="section-title">上传图片(选填最多9张)</text>
<view class="upload-container">
<view @click="upLoadImg()" class="upload-item add-item">
<image :src="Service.GetpayImg('/static/userFunc/comment/add.png')" class="add-icon"></image>
</view>
<view class="" v-for="(imgItem,imgIndex) in commentImgs" :key="imgIndex" style="position: relative;">
<image :src="Service.GetMateUrlByImg(imgItem)" class="upload-img"></image>
<image @click="closeImg(imgItem)" :src="Service.GetpayImg('/static/userFunc/comment/close.png')"
style="position: absolute; top: -6rpx; right: -10rpx; width: 20rpx; height: 20rpx; " alt="" />
</view>
</view>
</view>
<!-- 操作按钮 -->
<view class="action-buttons">
<up-button @click="submitReview" :customStyle='{borderRadius: "12rpx"}' color='#FF6A00' text="发布评价"></up-button>
</view>
</view>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { Service } from '@/Service/Service'
import { onLoad, onShow } from '@dcloudio/uni-app';
import { OrderService } from '@/Service/api/OrderService';
let commentImgs = ref([])
// 评价内容
let reviewContent = ref('');
let source = ref(5)
let orderDetail = ref<Array<any>>([])
let orderId = ref('')
onLoad((data : any) => {
orderId.value = data.orderId
getData()
})
onShow(() => {
})
const getData = () => {
OrderService.GetOrderInfo(orderId.value).then(res => {
if (res.code == 0) {
orderDetail.value = res.data.orderDetail
} else {
Service.Msg(res.msg)
}
})
}
const upLoadImg = () => {
uni.chooseImage({
count: 9, // 最多选择1张图片
sizeType: ['original', 'compressed'], // 支持原图和压缩图
sourceType: ['album', 'camera'], // 可从相册选择或使用相机拍照
success: function (res) {
let path = res.tempFiles[0].path
Service.uploadH5(path, 'Avatar', data => {
commentImgs.value.push(JSON.parse(data).data.path)
})
},
fail: function (err) {
console.error('选择失败:', err.errMsg);
}
})
}
const closeImg = (item:never) => {
let index = commentImgs.value.indexOf(item)
commentImgs.value.splice(index, 1)
console.log(commentImgs.value);
}
// 提交评价
const submitReview = () => {
if( reviewContent.value=='' ){
Service.Msg('评论不能为空!')
return
}
let data={
orderId:orderId.value,
source:source.value,
sign:reviewContent.value,
img:JSON.stringify(commentImgs.value)
}
OrderService.EvaluateOrder(data).then(res=>{
if(res.code==0){
Service.Msg(res.data)
setTimeout(()=>{
Service.GoPageBack()
},1000)
}else{
Service.Msg(res.msg)
}
})
};
</script>
<style scoped lang="scss">
/* 页面基础样式 */
.review-page {
background-color: #f5f5f5;
}
/* 商品信息 */
.product-info {
display: flex;
padding: 30rpx;
background-color: #fff;
margin-bottom: 20rpx;
}
.product-image {
width: 160rpx;
height: 160rpx;
border-radius: 16rpx;
}
.product-details {
margin-left: 24rpx;
flex: 1;
}
.product-name {
font-size: 28rpx;
color: #333;
line-height: 40rpx;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.product-attributes {
font-size: 24rpx;
color: #999;
margin-top: 10rpx;
}
/* 评分区域 */
.rating-section {
background-color: #fff;
padding: 30rpx;
margin-bottom: 20rpx;
}
.rating-item {
display: flex;
align-items: end;
justify-content: space-between;
margin-bottom: 30rpx;
}
.rating-item:last-child {
margin-bottom: 0;
}
.rating-label {
font-size: 28rpx;
color: #333;
font-weight: 600;
width: 160rpx;
}
.stars-container {
display: flex;
margin-left: 20rpx;
margin-right: 30rpx;
}
.star-icon {
width: 44rpx;
height: 44rpx;
margin-right: 10rpx;
}
.rating-value {
font-size: 28rpx;
color: #ff7d00;
font-weight: bold;
}
/* 评价标签 */
.tags-section {
background-color: #fff;
padding: 30rpx;
margin-bottom: 20rpx;
}
.section-title {
font-size: 28rpx;
color: #4B5563;
font-weight: bold;
margin-bottom: 20rpx;
}
.tags-container {
display: flex;
flex-wrap: wrap;
gap: 20rpx;
}
.tag-item {
font-size: 26rpx;
color: #666;
background-color: #f5f5f5;
padding: 10rpx 30rpx;
border-radius: 30rpx;
}
.tag-item.active {
background-color: #FF6A00;
color: #fff;
}
/* 评价心得 */
.review-content-section {
background-color: #fff;
padding: 30rpx;
margin-bottom: 20rpx;
}
.review-input {
width: 100%;
height: 240rpx;
border: 1rpx solid #eee;
border-radius: 16rpx;
padding: 20rpx;
font-size: 26rpx;
color: #333;
box-sizing: border-box;
resize: none;
}
.character-count {
font-size: 24rpx;
color: #999;
text-align: right;
margin-top: 10rpx;
}
/* 上传图片 */
.upload-section {
background-color: #fff;
padding: 30rpx;
margin-bottom: 20rpx;
}
.upload-container {
display: flex;
flex-wrap: wrap;
gap: 20rpx;
margin-top: 20rpx;
}
.upload-item {
width: 140rpx;
height: 140rpx;
border-radius: 16rpx;
overflow: hidden;
position: relative;
}
.add-item {
background-color: #F9FAFB;
display: flex;
align-items: center;
justify-content: center;
border: 1rpx dashed #D1D5DB;
}
.add-icon {
width: 40rpx;
height: 40rpx;
}
.upload-img {
width: 140rpx;
height: 140rpx;
border-radius: 10rpx;
}
/* 匿名评价 */
.anonymous-section {
background-color: #fff;
padding: 30rpx;
margin-bottom: 20rpx;
display: flex;
align-items: center;
}
.anonymous-checkbox {
width: 32rpx;
height: 32rpx;
}
.anonymous-label {
font-size: 28rpx;
color: #333;
margin-left: 16rpx;
}
/* 操作按钮 */
.action-buttons {
display: flex;
padding: 20rpx 30rpx;
gap: 20rpx;
position: fixed;
bottom: 0;
left: 0;
right: 0;
background-color: #fff;
box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.05);
}
</style>

View File

@@ -0,0 +1,192 @@
<template>
<view class="page">
<!-- 搜索框区域 -->
<view class="search-container">
<up-search @search="searchData() " v-model="searchText" :background="'#f5f5f5'" :showAction='false'
:input-style="{ fontSize: '28rpx', height: '70rpx' }" </up-search>
</view>
<!-- 搜索历史 -->
<view class="search-history">
<view class="history-header">
<text class="history-title">搜索历史</text>
<text class="clear-btn" @click="clearHistory">清除</text>
</view>
<view v-if="searchHistory.length>0" class="history-tags">
<view @click="Service.GoPage('/pages/userFunc/searchList?name=' + item)" class="history-tags-item"
v-for="(item, index) in searchHistory" :key="index">
{{item}}
</view>
</view>
<view v-else class="history-tags" style="font-size:28rpx ; color: #4B5563;">
暂无搜索历史
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { Service } from '@/Service/Service'
import { onLoad, onShow } from '@dcloudio/uni-app';
// 搜索文本
const searchText = ref('');
// 搜索历史
const searchHistory = ref<Array<any>>([]);
onLoad(() => {
})
onShow(() => {
if (Service.GetStorageCache('search')) {
searchHistory.value = Service.GetStorageCache('search')
}
})
// 清除搜索历史
const clearHistory = () => {
searchHistory.value = [];
Service.SetStorageCache('search', searchHistory.value)
};
const searchData = () => {
let data = Service.GetStorageCache('search')
if (searchHistory.value.includes(searchText.value)) {
console.log(1111);
Service.GoPage('/pages/userFunc/searchList?name=' + searchText.value)
return
}
if (data.length > 6) {
searchHistory.value.shift()
searchHistory.value.push(searchText.value)
Service.SetStorageCache('search', searchHistory.value)
} else {
searchHistory.value.push(searchText.value)
Service.SetStorageCache('search', searchHistory.value)
}
Service.GoPage('/pages/userFunc/searchList?name=' + searchText.value)
}
</script>
<style scoped lang="scss">
.page {
background-color: #fff;
min-height: 100vh;
}
/* 搜索框样式 */
.search-container {
padding: 24rpx;
}
.search-icons {
display: flex;
align-items: center;
}
.icon-item {
margin-left: 20rpx;
}
/* 搜索历史样式 */
.search-history {
padding: 20rpx 24rpx 20rpx;
background-color: #ffffff;
border-bottom: 1rpx solid #e2e2e2;
border-top: 1rpx solid #e2e2e2;
}
.history-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24rpx;
}
.history-title {
font-size: 30rpx;
color: #333333;
font-weight: 600;
}
.clear-btn {
font-size: 28rpx;
color: #666666;
}
.history-tags {
display: flex;
flex-wrap: wrap;
}
.history-tags-item {
display: flex;
align-items: center;
justify-content: center;
padding: 8rpx 30rpx;
border-radius: 30rpx;
margin-right: 15rpx;
background-color: #F3F4F6;
color: #4B5563;
margin-top: 15rpx;
font-size: 28rpx;
}
/* 推荐区域样式 */
.recommendation-section {
background-color: #ffffff;
padding: 30rpx 24rpx;
}
.section-title {
font-size: 32rpx;
color: #333333;
font-weight: bold;
margin-bottom: 30rpx;
}
/* 商品网格样式 */
.goods-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 24rpx;
margin-top: 18rpx;
}
.goods-item {
display: flex;
flex-direction: column;
border-radius: 16rpx;
overflow: hidden;
box-shadow: 0 0 10rpx 2rpx #e2e2e2;
}
.goods-img {
width: 100%;
height: 360rpx;
margin-bottom: 20rpx;
}
.goods-name {
flex: 1;
padding: 15rpx;
font-size: 28rpx;
color: #333333;
line-height: 40rpx;
margin-bottom: 10rpx;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.goods-price {
padding: 15rpx;
font-size: 32rpx;
color: #ff4d4f;
font-weight: bold;
}
</style>

View File

@@ -0,0 +1,61 @@
<template>
<view class="box">
<view class="" >
<view class="" style="font-weight: bold; font-size: 34rpx;" >
{{ data.title}}
</view>
<view class="" style="font-size: 30rpx;" >
{{ data.fubiaoti}}
</view>
<view class="" style="margin-top: 4rpx; font-size: 24rpx; color: #9f9f9f;" >
{{ Service.formatDate(data.addTime,1) }}
</view>
</view>
<rich-text :nodes="notes" style="margin-top: 20rpx;" ></rich-text>
</view>
</template>
<script setup lang="ts">
import { onShow, onLoad } from "@dcloudio/uni-app";
import { ref } from "vue";
import { Service } from '@/Service/Service';
import { AnnouncementService } from "@/Service/api/AnnouncementService"
let data=ref<any>({})
let notes = ref('')
let id=ref('')
onLoad((data:any) => {
id.value=data.id
getData( )
});
onShow(() => {
});
const getData=()=>{
AnnouncementService.GetAnnouncementInfo(id.value).then(res=>{
notes.value=res.data.info.content
data.value=res.data.info
})
}
</script>
<style lang="scss">
page{
background-color: #fff;
}
.box {
padding: 20rpx;
:deep(img) {
width: 100% !important;
height: auto !important;
display: block;
}
}
</style>

View File

@@ -0,0 +1,282 @@
<template>
<view class="notice-list-page">
<view class="notice-list-section">
<view v-if="topAnnoun.length>0" class="section-wrap">
<view class="section-header">
<view class="section-title-wrap">
<view class="section-dot"></view>
<text class="section-title">置顶公告</text>
</view>
</view>
<view class="notice-list">
<view v-for="(item, index) in topAnnoun" :key="index" class="notice-card" @click="goToNoticeDetail(item)">
<view class="notice-image-wrapper">
<image :src="Service.GetMateUrlByImg(item.img)" class="notice-image" mode="aspectFill"></image>
<view class="notice-tag">置顶</view>
</view>
<view class="notice-info">
<text class="notice-title">{{ item.title }}</text>
<text class="notice-subtitle">{{ item.fubiaoti }}</text>
<view class="notice-meta">
<text class="notice-time">{{ Service.formatDate(item.addTime, 1) }}</text>
</view>
</view>
</view>
</view>
</view>
<view class="section-wrap">
<view class="section-header">
<view class="section-title-wrap">
<view class="section-dot"></view>
<text class="section-title">公告列表</text>
</view>
</view>
<view v-if="noticeList.length > 0" class="notice-list">
<view v-for="(item, index) in noticeList" :key="index" class="notice-card" @click="goToNoticeDetail(item)">
<view class="notice-image-wrapper">
<image :src="Service.GetMateUrlByImg(item.img)" class="notice-image" mode="aspectFill"></image>
</view>
<view class="notice-info">
<text class="notice-title">{{ item.title }}</text>
<text class="notice-subtitle">{{ item.fubiaoti }}</text>
<view class="notice-meta">
<text class="notice-time">{{ Service.formatDate(item.addTime, 1) }}</text>
</view>
</view>
</view>
</view>
<view v-else class="empty-state">
<image :src="Service.GetpayImg('/static/userFunc/null.png')" class="empty-image"></image>
<text class="empty-text">暂无公告</text>
</view>
</view>
<view class="load-more" v-if="noticeList.length > 0">
<up-loadmore :status="status" />
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { onShow, onLoad, onReachBottom } from "@dcloudio/uni-app";
import { ref } from "vue";
import { Service } from '@/Service/Service';
import { AnnouncementService } from "@/Service/api/AnnouncementService"
let noticeList = ref<any[]>([]);
let topAnnoun = ref<Array<any>>([])
let page = ref(1)
let status = ref('nomore')
onLoad(() => {
getData();
});
onShow(() => {
});
onReachBottom(() => {
getList()
})
const getData = () => {
noticeList.value = []
page.value = 1
status.value = 'loadmore'
getList()
};
const getList = () => {
if (status.value == 'loading' || status.value == 'nomore') {
return
}
status.value = 'loading'
AnnouncementService.GetAnnouncementList(page.value).then(res => {
if (res.code == 0) {
if (page.value == 1) {
topAnnoun.value = res.data.topAnnoun
}
noticeList.value = [...noticeList.value, ...res.data.list]
status.value = res.data.list.length == 10 ? 'loadmore' : 'nomore'
page.value++
} else {
Service.Msg(res.msg)
}
})
}
const goToNoticeDetail = (item: any) => {
Service.GoPage('/pages/userFunc/notice?id=' + item.announcementId)
};
</script>
<style scoped lang="scss">
.notice-list-page {
min-height: 100vh;
padding-bottom: 40rpx;
background: #F8F9FA;
}
/* ===== 列表区域 ===== */
.notice-list-section {
padding: 30rpx 30rpx 0;
}
.section-wrap {
margin-bottom: 30rpx;
}
.section-header {
padding: 0 0 24rpx;
}
.section-title-wrap {
display: flex;
align-items: center;
gap: 14rpx;
}
.section-dot {
width: 8rpx;
height: 32rpx;
border-radius: 4rpx;
background: linear-gradient(180deg, var(--nav-mian), var(--nav-vice));
flex-shrink: 0;
}
.section-title {
font-size: 32rpx;
font-weight: 700;
color: #1F2937;
}
/* ===== 公告卡片 ===== */
.notice-list {
display: flex;
flex-direction: column;
gap: 20rpx;
}
.notice-card {
display: flex;
align-items: center;
background-color: #fff;
border-radius: 24rpx;
padding: 20rpx;
box-shadow: 0 2rpx 16rpx rgba(0, 0, 0, 0.04);
transition: all 0.2s ease;
}
.notice-card:active {
transform: scale(0.98);
box-shadow: 0 1rpx 8rpx rgba(0, 0, 0, 0.04);
}
.notice-image-wrapper {
width: 160rpx;
height: 160rpx;
flex-shrink: 0;
margin-right: 20rpx;
position: relative;
border-radius: 16rpx;
overflow: hidden;
background: linear-gradient(135deg, rgba(255, 106, 0, 0.06), rgba(255, 127, 0, 0.04));
}
.notice-image {
width: 100%;
height: 100%;
display: block;
object-fit: cover;
}
.notice-tag {
position: absolute;
top: 0;
left: 0;
font-size: 18rpx;
font-weight: 700;
color: #fff;
background: linear-gradient(135deg, #FF6A00, #FF4500);
padding: 6rpx 16rpx;
border-radius: 10rpx 0 20rpx 0;
line-height: 1.2;
z-index: 1;
letter-spacing: 1rpx;
}
.notice-info {
flex: 1;
display: flex;
flex-direction: column;
min-width: 0;
padding: 6rpx 0;
}
.notice-title {
font-size: 28rpx;
font-weight: 600;
color: #1F2937;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
line-height: 1.4;
margin-bottom: 8rpx;
}
.notice-subtitle {
font-size: 24rpx;
color: #9CA3AF;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
line-height: 1.4;
margin-bottom: 12rpx;
}
.notice-meta {
display: flex;
align-items: center;
justify-content: space-between;
}
.notice-time {
font-size: 22rpx;
color: #C0C4CC;
}
/* ===== 空状态 ===== */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
padding: 120rpx 20rpx;
background: #fff;
border-radius: 24rpx;
box-shadow: 0 2rpx 16rpx rgba(0, 0, 0, 0.04);
}
.empty-image {
width: 240rpx;
height: 240rpx;
margin-bottom: 24rpx;
}
.empty-text {
font-size: 28rpx;
color: #C0C4CC;
font-weight: 500;
}
/* ===== 加载更多 ===== */
.load-more {
margin-top: 20rpx;
}
</style>

View File

@@ -0,0 +1,485 @@
<template>
<view class="promotion-page">
<!-- <view class="stats-section">
<view class="stats-card">
<view class="stats-decoration-left"></view>
<view class="stats-decoration-right"></view>
<view class="stats-content">
<view class="stat-item" @click="openPromotePopup">
<up-icon name="share" size='24' color='#FF6A00' bold='true'></up-icon>
<text class="stat-label">推广码</text>
</view>
<view class="stat-divider"></view>
<view class="stat-item">
<text class="stat-value">{{ totalMembers }}</text>
<text class="stat-label">推广</text>
</view>
</view>
</view>
</view> -->
<view class="content-section">
<view class="section-header">
<text class="section-title">推广会员列表</text>
<text class="section-count"> {{ totalMembers }} </text>
</view>
<view v-if="membersList.length > 0" class="members-list">
<view v-for="(item, index) in membersList" :key="index" class="member-card">
<view class="member-card-bg"></view>
<view class="member-content">
<view class="member-avatar-wrapper">
<image :src="Service.GetMateUrlByImg(item.avatar)" class="member-avatar" mode="aspectFill">
</image>
</view>
<view class="member-info">
<view class="member-name-row">
<text class="member-name">{{ item.nick || '神秘用户' }}</text>
<view class="join-tag">
<text class="join-text">{{ item.conLev>0?'铁粉':'粉丝' }}</text>
</view>
</view>
<view class="" style="display: flex; align-items: center; justify-content: space-between;">
<view class="member-phone">
<text class="phone-text">{{ item.phone || '暂无手机号' }}</text>
</view>
<text class="time-value">{{ Service.formatDate(item.addTime, 1) }}</text>
</view>
</view>
</view>
</view>
</view>
<view v-else class="empty-state">
<image :src="Service.GetpayImg('/static/userFunc/null.png')" class="empty-image"></image>
<text class="empty-text">暂无推广人员</text>
<text class="empty-desc">邀请好友加入获得积分奖励</text>
</view>
</view>
<view v-if="membersList.length > 0 && status !== 'nomore'" class="load-more">
<up-loadmore :status="status"></up-loadmore>
</view>
<up-popup :show="showPromotePopup" mode="center" round="16" @close="closePromotePopup"
:closeOnClickOverlay="true">
<view class="popup-content" style="border-radius: 20rpx;">
<view class="popup-close" @click="closePromotePopup">
<up-icon name="close" size="18" color="#999"></up-icon>
</view>
<view class="" style="display: flex; align-items: center; gap: 10rpx; ">
<view class="qrcode-wrapper">
<up-qrcode cid="ex1" :size="180" :val="qrcodeUrl"></up-qrcode>
</view>
</view>
<text class="popup-tip">扫描二维码即可加入</text>
</view>
</up-popup>
</view>
</template>
<script setup lang="ts">
import { onShow, onLoad, onReachBottom } from "@dcloudio/uni-app";
import { ref } from "vue";
import { Service } from '@/Service/Service';
import { UserService } from "@/Service/api/UserService"
let page = ref(1);
let status = ref('nomore');
let totalMembers = ref(0);
let showPromotePopup = ref(false);
let qrcodeUrl = ref('');
let membersList = ref<any[]>([]);
onLoad(() => {
getData();
geyUrl()
});
onShow(() => {
});
onReachBottom(() => {
getMembersList();
});
const geyUrl = () => {
// UserService.GetUserRemUrl().then(res => {
// if (res.code == 0) {
// qrcodeUrl.value = res.data.url
// } else {
// Service.Msg(res.msg)
// }
// })
}
const getData = () => {
membersList.value = []
page.value = 1
status.value = 'loadmore'
getMembersList()
}
const getMembersList = () => {
if (status.value == 'loading' || status.value == 'nomore') {
return;
}
status.value = 'loading';
UserService.GetUserRemList(page.value).then(res => {
if (res.code == 0) {
if (page.value == 1) {
totalMembers.value = res.data.total.value
}
membersList.value = [...membersList.value, ...res.data.remList]
status.value = res.data.remList.length == 10 ? 'loadmore' : 'nomore'
page.value++
} else {
Service.Msg(res.msg)
}
})
};
const openPromotePopup = () => {
showPromotePopup.value = true;
};
const closePromotePopup = () => {
showPromotePopup.value = false;
};
</script>
<style scoped lang="scss">
page {
background: linear-gradient(180deg, #FFF7ED 0%, #FFF5EB 50%, #F5F5F5 100%);
}
.stats-section {
padding: 20rpx 30rpx 0;
position: relative;
z-index: 2;
}
.stats-card {
background: linear-gradient(145deg, #FFFFFF 0%, #FFF9F2 100%);
border-radius: 24rpx;
padding: 40rpx 30rpx;
box-shadow: 0 8rpx 30rpx rgba(255, 106, 0, 0.12);
position: relative;
overflow: hidden;
}
.stats-decoration-left {
position: absolute;
top: -40rpx;
left: -40rpx;
width: 120rpx;
height: 120rpx;
border-radius: 50%;
background: linear-gradient(135deg, rgba(255, 106, 0, 0.08), rgba(255, 127, 0, 0.03));
}
.stats-decoration-right {
position: absolute;
bottom: -30rpx;
right: -30rpx;
width: 100rpx;
height: 100rpx;
border-radius: 50%;
background: linear-gradient(135deg, rgba(255, 127, 0, 0.06), rgba(255, 106, 0, 0.02));
}
.stats-content {
display: flex;
align-items: center;
justify-content: space-around;
position: relative;
z-index: 1;
}
.stat-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 10rpx;
}
.stat-value {
font-size: 44rpx;
font-weight: bold;
color: #FF6A00;
line-height: 1;
}
.stat-label {
font-size: 24rpx;
color: #666;
}
.stat-divider {
width: 1rpx;
height: 60rpx;
background: rgba(255, 106, 0, 0.2);
}
.content-section {
padding: 30rpx 20rpx 0;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
}
.section-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
}
.section-count {
font-size: 26rpx;
color: #999;
}
.members-list {
display: flex;
flex-direction: column;
gap: 20rpx;
}
.member-card {
background-color: #fff;
border-radius: 24rpx;
overflow: hidden;
position: relative;
}
.member-card-bg {
position: absolute;
top: 0;
right: 0;
width: 200rpx;
height: 200rpx;
background: linear-gradient(135deg, rgba(255, 106, 0, 0.05), rgba(255, 127, 0, 0.02));
border-radius: 50%;
transform: translate(30%, -30%);
}
.member-content {
display: flex;
align-items: center;
padding: 28rpx 30rpx;
position: relative;
z-index: 1;
}
.member-avatar-wrapper {
width: 100rpx;
height: 100rpx;
border-radius: 50%;
overflow: hidden;
position: relative;
flex-shrink: 0;
margin-right: 24rpx;
}
.avatar-border {
position: absolute;
top: -3rpx;
left: -3rpx;
right: -3rpx;
bottom: -3rpx;
border-radius: 50%;
border: 3rpx solid linear-gradient(135deg, #FF6A00, #FF7F00);
background: linear-gradient(135deg, #FF6900, #FF7F00);
z-index: -1;
padding: 3rpx;
}
.member-avatar {
width: 100%;
height: 100%;
border-radius: 50%;
}
.member-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 12rpx;
}
.member-name-row {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 6rpx;
}
.member-name {
font-size: 32rpx;
font-weight: 600;
color: #1A1A1A;
}
.join-tag {
background: linear-gradient(135deg, #FF6A00, #FF7F00);
padding: 6rpx 16rpx;
border-radius: 8rpx;
display: flex;
align-items: center;
}
.join-text {
font-size: 20rpx;
color: #fff;
font-weight: 500;
}
.member-phone {
display: flex;
align-items: center;
gap: 8rpx;
}
.phone-text {
font-size: 24rpx;
color: #666;
}
.member-time {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 4rpx;
flex-shrink: 0;
}
.time-label {
font-size: 20rpx;
color: #999;
}
.time-value {
font-size: 24rpx;
color: #FF6A00;
font-weight: 500;
}
.reward-info {
display: flex;
flex-direction: column;
align-items: center;
gap: 6rpx;
padding: 16rpx 20rpx;
background: linear-gradient(145deg, #FFF4E6 0%, #FFF9F2 100%);
border-radius: 12rpx;
margin-left: 16rpx;
flex-shrink: 0;
}
.reward-label {
font-size: 20rpx;
color: #FF9800;
}
.reward-value {
font-size: 28rpx;
font-weight: 700;
color: #FF6A00;
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
padding: 80rpx 20rpx;
background-color: #fff;
border-radius: 20rpx;
}
.empty-image {
width: 200rpx;
height: 200rpx;
margin-bottom: 30rpx;
opacity: 0.5;
}
.empty-text {
font-size: 32rpx;
color: #333;
margin-bottom: 10rpx;
}
.empty-desc {
font-size: 26rpx;
color: #999;
}
.load-more {
margin-top: 20rpx;
}
.popup-content {
width: 560rpx;
padding: 50rpx 40rpx;
background: linear-gradient(180deg, #FFFFFF 0%, #FFF9F2 100%);
position: relative;
display: flex;
flex-direction: column;
align-items: center;
}
.popup-close {
position: absolute;
top: 20rpx;
right: 20rpx;
width: 48rpx;
height: 48rpx;
display: flex;
align-items: center;
justify-content: center;
;
}
.popup-text {
font-size: 30rpx;
color: #FF6A00;
font-weight: 500;
text-align: center;
line-height: 1.6;
margin-bottom: 40rpx;
}
.qrcode-wrapper {
width: 360rpx;
height: 360rpx;
background: #fff;
border-radius: 20rpx;
padding: 20rpx;
box-shadow: 0 8rpx 30rpx rgba(255, 106, 0, 0.15);
margin-bottom: 30rpx;
}
.qrcode-image {
width: 100%;
height: 100%;
}
.popup-tip {
font-size: 24rpx;
color: #FF7F00;
}
</style>

View File

@@ -0,0 +1,235 @@
<template>
<view class="search-page">
<!-- 搜索结果 -->
<view class="search-content">
<view v-if="leftList.length > 0" class="goods-waterfall">
<view class="waterfall-col">
<view class="goods-card" v-for="(item, index) in leftList" :key="index"
@click="Service.GoPage('/pages/webView/goodsDetail?goodsId='+item.goodsId)">
<view class="goods-img-wrap">
<image :src="Service.GetMateUrlByImg(item.goodsImg)" mode="widthFix" class="goods-img"></image>
</view>
<view class="goods-info">
<text class="goods-name">{{ item.goodsName }}</text>
<view class="goods-price-row">
<text class="goods-price">¥<text class="price-num">{{ item.price }}</text></text>
</view>
</view>
</view>
</view>
<view class="waterfall-col">
<view class="goods-card" v-for="(item, index) in rightList" :key="index"
@click="Service.GoPage('/pages/webView/goodsDetail?goodsId='+item.goodsId)">
<view class="goods-img-wrap">
<image :src="Service.GetMateUrlByImg(item.goodsImg)" mode="widthFix" class="goods-img"></image>
</view>
<view class="goods-info">
<text class="goods-name">{{ item.goodsName }}</text>
<view class="goods-price-row">
<text class="goods-price">¥<text class="price-num">{{ item.price }}</text></text>
</view>
</view>
</view>
</view>
</view>
<view v-else class="empty-state">
<image :src="Service.GetpayImg('/static/userFunc/null.png')" class="empty-image"></image>
<text class="empty-text">暂无相关商品</text>
</view>
<view class="load-more" v-if="leftList.length > 0">
<up-loadmore :status="status" />
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { onShow, onLoad } from "@dcloudio/uni-app";
import { Service } from "@/Service/Service"
import { ref } from "vue";
import { GoodsService } from '@/Service/api/GoodsService';
let page = ref(1)
let status = ref('nomore')
let leftList = ref<Array<any>>([])
let rightList = ref<Array<any>>([])
let search = ref('')
onLoad((data: any) => {
search.value = data.name
uni.setNavigationBarTitle({
title: search.value
})
getData()
});
const getData = () => {
leftList.value = []
rightList.value = []
page.value = 1
status.value = 'loadmore'
getList()
}
const getList = () => {
if (status.value == 'loading' || status.value == 'nomore') {
return
}
status.value = 'loading'
GoodsService.GetSerchGoodsList(search.value, page.value).then(res => {
if (res.code == 0) {
res.data.list.map((item: any, index: any) => {
if (index % 2 == 1) {
rightList.value.push(item)
} else {
leftList.value.push(item)
}
})
status.value = res.data.list.length == 10 ? 'loadmore' : 'nomore'
page.value++
} else {
Service.Msg(res.msg)
}
})
}
</script>
<style lang="scss">
page {
background-color: #F8F9FA;
}
.search-page {
min-height: 100vh;
background: #F8F9FA;
}
/* ===== 搜索栏 ===== */
.search-bar {
background: #fff;
padding: 16rpx 20rpx;
padding-top: calc(16rpx + env(safe-area-inset-top));
position: relative;
z-index: 10;
box-shadow: 0 2rpx 16rpx rgba(0, 0, 0, 0.04);
}
.search-bar-inner {
display: flex;
align-items: center;
gap: 20rpx;
}
.search-input-wrap {
flex: 1;
}
/* ===== 搜索结果 ===== */
.search-content {
padding: 20rpx 20rpx 0;
}
/* ===== 商品瀑布流 ===== */
.goods-waterfall {
display: flex;
gap: 16rpx;
}
.waterfall-col {
width: 50%;
display: flex;
flex-direction: column;
gap: 16rpx;
}
.goods-card {
background: #fff;
border-radius: 20rpx;
overflow: hidden;
box-shadow: 0 2rpx 16rpx rgba(0, 0, 0, 0.04);
transition: transform 0.2s, box-shadow 0.2s;
}
.goods-card:active {
transform: scale(0.97);
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.06);
}
.goods-img-wrap {
position: relative;
width: 100%;
overflow: hidden;
}
.goods-img {
width: 100%;
display: block;
background-color: #f5f5f5;
}
.goods-info {
padding: 16rpx 16rpx 20rpx;
}
.goods-name {
font-size: 26rpx;
color: #1F2937;
line-height: 1.5;
font-weight: 500;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
text-overflow: ellipsis;
}
.goods-price-row {
margin-top: 12rpx;
display: flex;
align-items: center;
}
.goods-price {
font-weight: 700;
font-size: 28rpx;
color: #FF6A00;
}
.price-num {
font-size: 32rpx;
}
/* ===== 空状态 ===== */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
padding: 160rpx 20rpx;
}
.empty-image {
width: 280rpx;
height: 280rpx;
margin-bottom: 24rpx;
opacity: 0.6;
}
.empty-text {
font-size: 28rpx;
color: #C0C4CC;
font-weight: 500;
}
/* ===== 加载更多 ===== */
.load-more {
margin-top: 20rpx;
padding-bottom: 40rpx;
}
</style>

253
src/pages/userFunc/set.vue Normal file
View File

@@ -0,0 +1,253 @@
<template>
<view class="page">
<!-- 用户信息卡片 -->
<view @click="Service.GoPage('/pages/userFunc/setUserData')" class="user-card">
<img v-if="userInfo.avatar" :src="Service.GetMateUrlByImg(userInfo.avatar)"
style="width: 100rpx; height: 100rpx; border-radius: 50%; border: 2rpx solid #fff; " alt="" />
<view class="user-info">
<text class="user-name">{{ userInfo.nick }}</text>
<view style="color: #fff; font-size: 24rpx; " >{{ phone(userInfo.phone) }}</view>
</view>
<u-icon name="arrow-right" size="34rpx" color="#fff"></u-icon>
</view>
<!-- 设置列表 -->
<view class="settings-list">
<!-- 账户安全 -->
<view class="settings-section">
<view class="section-title">账户安全</view>
<view v-for="(accountItem,accountIndex) in account" :key="accountIndex"
@click="Service.GoPage(accountItem.path)" class="section-item">
<view class="" style="display: flex; align-items: center;">
<view class="icon" style=" ">
<img class="icon-img" :src="Service.GetpayImg(accountItem.icon)" alt="" />
</view>
<text style="margin-left: 14rpx; font-weight: 500; ">{{accountItem.name}}</text>
</view>
</view>
</view>
<!-- 退出登录按钮 -->
<view class=""
style=" width: 100%; position: fixed; bottom: 0; left: 0; background-color: #f5f5f5; padding: 20rpx 30rpx; ">
<view class="" @click="handleLogout()"
style="width: 100%; color: #fff; background-color: #FF6A00; padding: 20rpx ; text-align: center; border-radius: 20rpx; ">
退出登录
</view>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { Service } from '@/Service/Service';
import { UserService } from "@/Service/api/UserService"
import { onLoad, onShow } from '@dcloudio/uni-app';
const userInfo = ref({
avatar: '',
nick: '张小明',
phone: '',
province: '',
city: '',
county: ''
});
onLoad(()=>{
getUserInfo();
})
const getUserInfo = () => {
UserService.GetUserInfo().then(res => {
if (res.code == 0) {
userInfo.value = res.data.info;
}
});
};
const account = ref([
{
name: '修改密码',
icon: '/static/userFunc/set/change.png',
path: '/pages/login/changgePassword'
},
{
name: '修改手机号',
icon: '/static/userFunc/set/phone.png',
path: '/pages/login/changePhone'
},
{
name: '修改支付密码',
icon: '/static/userFunc/set/password.png',
path: '/pages/login/changePay'
}
])
const phone=(data:any)=>{
return String(data).slice(0, 3) + '****' + String(data).slice(-4)
}
const handleLogout = () => {
uni.showModal({
title: '退出登录',
content: '确定要退出当前账号吗?',
success: (res) => {
if (res.confirm) {
Service.OffUserToken()
Service.GoPageDelse('/pages/login/login')
}
}
});
};
</script>
<style scoped lang="scss">
.page {
background-color: #f5f5f5;
}
// 图标
.icon {
width: 50rpx;
height: 50rpx;
border-radius: 50%;
background-color: #FFEDD5;
display: flex;
align-items: center;
justify-content: center;
}
.icon-img {
width: 32rpx;
height: 32rpx;
}
/* 用户信息卡片 */
.user-card {
margin: 30rpx;
border-radius: 20rpx;
background: linear-gradient(to right, #F97316, #FB923C);
padding: 40rpx 30rpx;
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 30rpx;
}
.user-avatar {
background-color: #FFFFFF;
padding: 10rpx;
}
.user-info {
margin-left: 30rpx;
flex: 1;
}
.user-name {
font-size: 32rpx;
color: #FFFFFF;
font-weight: 500;
margin-bottom: 10rpx;
display: block;
}
.user-level {
width: fit-content;
font-size: 18rpx;
color: #fff;
background-color: rgba(255, 255, 255, 0.2);
padding: 4rpx 20rpx;
border-radius: 24rpx;
}
/* 设置列表 */
.settings-list {
padding: 0 30rpx;
}
.settings-section {
background-color: #FFFFFF;
border-radius: 20rpx;
margin-bottom: 30rpx;
overflow: hidden;
padding: 30rpx;
}
.section-title {
font-size: 28rpx;
color: #6B7280;
font-weight: 600;
}
.section-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20rpx 0;
border-bottom: 1rpx solid #e2e2e2;
&:last-child {
border-bottom: none;
padding-bottom: 0;
}
}
.u-cell {
height: 100rpx;
font-size: 32rpx;
--u-cell-value-color: #999999;
--u-cell-title-color: #333333;
--u-cell-arrow-size: 40rpx;
}
/* 版本信息 */
.version-info {
display: flex;
justify-content: space-between;
align-items: center;
padding: 30rpx;
background-color: #FFFFFF;
border-radius: 20rpx;
margin-bottom: 60rpx;
}
.version-text {
font-size: 28rpx;
color: #333333;
}
.version-number {
font-size: 28rpx;
color: #999999;
margin-right: 10rpx;
}
.check-update {
font-size: 24rpx;
color: #FF6600;
background-color: #FFF7ED;
display: flex;
align-items: center;
justify-content: center;
padding: 6rpx 16rpx;
border-radius: 15rpx;
}
/* 退出登录按钮 */
.logout-btn {
margin: 0 30rpx;
}
</style>

View File

@@ -0,0 +1,269 @@
<template>
<view class="set-user-page">
<!-- 顶部装饰背景 -->
<view class="header-bg">
<view class="header-bg-inner"></view>
</view>
<!-- 头像区域 -->
<view class="avatar-section" @click="uploadFImg">
<view class="avatar-wrapper">
<img class="avatar-img" :src="Service.GetMateUrlByImg(info.avatar)" />
<!-- 相机图标 -->
<view class="avatar-camera">
<u-icon name="camera-fill" color="#fff" size="16"></u-icon>
</view>
</view>
<text class="avatar-tip">点击更换头像</text>
</view>
<!-- 表单卡片 -->
<view class="form-card">
<u-form labelWidth="90" labelPosition="left" :model="info" ref="form1" :borderBottom="true">
<u-form-item label="昵称" prop="info.nick" borderBottom>
<u-input inputAlign="right" v-model="info.nick" border="none" placeholder="请输入昵称" clearable
:customStyle="{ color: '#333' }"></u-input>
</u-form-item>
<u-form-item label="手机号" prop="info.phone" borderBottom>
<u-input inputAlign="right" disabled disabledColor="transparent" v-model="info.phone" border="none"
placeholder="未绑定" :customStyle="{ color: '#999' }"></u-input>
</u-form-item>
</u-form>
</view>
<!-- 底部保存按钮 -->
<view class="footer-safe">
<view class="save-btn-wrapper">
<u-button type="primary" shape="circle" size="large" color="linear-gradient(90deg, #FF7F00, #FF6A00)"
:customStyle="btnStyle" @click="save" text="保存信息"></u-button>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { onShow, onLoad } from "@dcloudio/uni-app";
import { Service } from "@/Service/Service";
import { ref } from "vue";
import { UserService } from "@/Service/api/UserService";
let info = ref({
avatar: "",
nick: "",
phone: "",
});
const btnStyle = {
fontWeight: 600,
fontSize: "32rpx",
boxShadow: "0 8rpx 24rpx rgba(255, 106, 0, 0.35)",
height: "96rpx",
lineHeight: "96rpx",
};
onLoad(() => { });
onShow(() => {
getData();
});
const getData = () => {
UserService.GetUserInfo().then((res) => {
if (res.code == 0) {
info.value = res.data.info;
} else {
Service.Msg(res.msg);
}
});
};
const uploadFImg = () => {
uni.chooseImage({
count: 1,
sizeType: ["original", "compressed"],
sourceType: ["album", "camera"],
success: function (res) {
let path = res.tempFiles[0].path;
Service.uploadH5(path, "Avatar", (data) => {
console.log(data);
info.value.avatar = JSON.parse(data).data.path;
});
},
fail: function (err) {
console.error("选择失败:", err.errMsg);
},
});
};
const save = () => {
if (info.value.avatar == "") {
Service.Msg("请上传头像!");
return;
}
if (info.value.nick == "") {
Service.Msg("请输入昵称!");
return;
}
UserService.UpdateUser(info.value.avatar, info.value.nick).then((res) => {
if (res.code == 0) {
Service.Msg("保存成功!");
setTimeout(() => {
Service.GoPageBack();
}, 1000);
} else {
Service.Msg(res.msg);
}
});
};
</script>
<style lang="scss">
page {
background-color: #f6f7f9;
}
.set-user-page {
position: relative;
min-height: 100vh;
padding-bottom: 160rpx;
box-sizing: border-box;
}
/* 顶部装饰背景 */
.header-bg {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 240rpx;
overflow: hidden;
z-index: 0;
.header-bg-inner {
width: 100%;
height: 100%;
background: linear-gradient(180deg, #fff5ec 0%, rgba(255, 245, 236, 0) 100%);
border-radius: 0 0 50% 50%;
transform: scaleX(1.4);
}
}
/* 头像区域 */
.avatar-section {
position: relative;
z-index: 1;
display: flex;
flex-direction: column;
align-items: center;
padding-top: 40rpx;
margin-bottom: 40rpx;
}
.avatar-wrapper {
position: relative;
width: 160rpx;
height: 160rpx;
border-radius: 50%;
background-color: #fff;
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.06);
display: flex;
align-items: center;
justify-content: center;
transition: transform 0.2s ease;
&:active {
transform: scale(0.96);
}
}
.avatar-img {
width: 160rpx;
height: 160rpx;
border-radius: 50%;
object-fit: cover;
border: 4rpx solid #fff;
box-sizing: border-box;
}
.avatar-empty {
width: 160rpx;
height: 160rpx;
border-radius: 50%;
background: #f0f0f0;
display: flex;
align-items: center;
justify-content: center;
}
.avatar-placeholder-icon {
width: 60rpx;
height: 60rpx;
opacity: 0.5;
}
.avatar-camera {
position: absolute;
bottom: 0;
right: 0;
width: 48rpx;
height: 48rpx;
background: linear-gradient(135deg, #FF7F00, #FF6A00);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
border: 4rpx solid #fff;
box-shadow: 0 4rpx 12rpx rgba(255, 106, 0, 0.3);
}
.avatar-tip {
margin-top: 20rpx;
font-size: 26rpx;
color: #999;
letter-spacing: 1rpx;
}
/* 表单卡片 */
.form-card {
position: relative;
z-index: 1;
margin: 0 30rpx;
padding: 0 30rpx;
background-color: #fff;
border-radius: 24rpx;
box-shadow: 0 4rpx 24rpx rgba(0, 0, 0, 0.04);
::v-deep .u-form-item__body__left__content__label {
font-size: 30rpx;
color: #333;
font-weight: 500;
}
::v-deep .u-input__content__field-wrapper__field {
font-size: 30rpx !important;
}
::v-deep .u-form-item__body__right__content__slot {
justify-content: flex-end;
}
}
/* 底部安全区 + 保存按钮 */
.footer-safe {
position: fixed;
left: 0;
bottom: 0;
width: 100%;
padding: 24rpx 40rpx calc(env(safe-area-inset-bottom) + 24rpx) 40rpx;
background: rgba(255, 255, 255, 0.85);
backdrop-filter: blur(12rpx);
z-index: 10;
box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.03);
}
.save-btn-wrapper {
width: 100%;
}
</style>

View File

@@ -0,0 +1,470 @@
<template>
<view class="">
<view class="transfer-section">
<view class="transfer-card">
<view class="form-item">
<text class="form-label">转账到</text>
<view class="type-options">
<view class="type-item" :class="{ active: transferType === 1 }" @click="transferType = 1">
<view class="type-radio">
<view v-if="transferType === 1" class="type-radio-dot"></view>
</view>
<view class="type-info">
<text class="type-name">报单积分</text>
</view>
</view>
<view class="type-item" :class="{ active: transferType === 2 }" @click="transferType = 2">
<view class="type-radio">
<view v-if="transferType === 2" class="type-radio-dot"></view>
</view>
<view class="type-info">
<text class="type-name">普通积分</text>
</view>
</view>
<view class="type-item" :class="{ active: transferType === 3 }" @click="transferType = 3">
<view class="type-radio">
<view v-if="transferType === 3" class="type-radio-dot"></view>
</view>
<view class="type-info">
<text class="type-name">自营积分</text>
</view>
</view>
</view>
</view>
<view class="form-item" style="margin-bottom: 10rpx;">
<text class="form-label">转账金额</text>
<view class="input-wrapper amount-wrapper">
<text class="currency-symbol">¥</text>
<input v-model="transferForm.amount" class="input-field amount-input" type="text"
placeholder="0.00" @input="handleAmountInput" />
</view>
<view class="amount-hint" style="margin-top: 20rpx;">
<text class="hint-text">当前余额</text>
<text class="available-amount">¥{{ accInfo.account }}</text>
</view>
</view>
</view>
</view>
<view class="bottom-action">
<button class="transfer-btn" @click="handleTransfer" :disabled="!canTransfer">
立即转账
</button>
</view>
<up-popup :show="showPasswordPopup" mode="center" :round="20" :closeOnClickOverlay="false">
<view class="password-popup">
<view class="popup-header">
<text class="popup-title">请输入支付密码</text>
<view class="popup-close" @click="closePasswordPopup">
<up-icon name="close" size="24" color="#999"></up-icon>
</view>
</view>
<view class="popup-content">
<view class="password-desc">转账金额¥{{ transferForm.amount || '0.00' }}</view>
<view class="password-input-wrapper">
<view v-for="(item, index) in 6" :key="index" class="password-box"
:class="{ active: passwordInput.length === index }">
<text v-if="passwordInput[index]" class="password-dot"></text>
</view>
</view>
<input ref="passwordInputRef" v-model="passwordInput" class="hidden-password-input" type="number"
maxlength="6" :focus="showPasswordPopup" @input="onPasswordInput" />
<view class="forget-password" @click="goToSetPassword">
<text class="forget-text">忘记密码</text>
</view>
</view>
</view>
</up-popup>
</view>
</template>
<script setup lang="ts">
import { ref, computed, nextTick } from 'vue';
import { onLoad } from '@dcloudio/uni-app';
import { Service } from '@/Service/Service';
import { UserService } from '@/Service/api/UserService';
const showPasswordPopup = ref(false);
const passwordInput = ref('');
const passwordInputRef = ref<any>(null);
const transferType = ref(1)
const transferForm = ref({
amount: ''
});
let accInfo=ref<any>({})
const canTransfer = computed(() => {
return transferForm.value.amount &&
parseFloat(transferForm.value.amount) > 0;
});
onLoad(() => {
getAccInfo()
});
const getAccInfo = () => {
UserService.GetUserAccInfo().then(res => {
if (res.code == 0) {
accInfo.value = res.data.accInfo;
} else {
Service.Msg(res.msg);
}
});
};
const handleAmountInput = (e : any) => {
let value = e.detail.value;
value = value.replace(/[^\d.]/g, '');
const parts = value.split('.');
if (parts.length > 2) {
value = parts[0] + '.' + parts.slice(1).join('');
}
if (parts.length === 2 && parts[1].length > 2) {
value = parts[0] + '.' + parts[1].substring(0, 2);
}
transferForm.value.amount = value;
};
const handleTransfer = () => {
if (!transferForm.value.amount) {
Service.Msg('请输入转账金额');
return;
}
if (parseFloat(transferForm.value.amount) <= 0) {
Service.Msg('转账金额必须大于0');
return;
}
passwordInput.value = '';
showPasswordPopup.value = true;
nextTick(() => {
if (passwordInputRef.value) {
passwordInputRef.value.focus();
}
});
};
const onPasswordInput = () => {
if (passwordInput.value.length === 6) {
submitTransfer();
}
};
const submitTransfer = () => {
showPasswordPopup.value = false;
Service.LoadIng('转账中...');
UserService.TranAccount(Number(transferForm.value.amount), transferType.value).then(res => {
if (res.code == 0) {
Service.LoadClose();
Service.Msg('转账成功!');
setTimeout(() => {
Service.GoPageBack();
}, 1000);
} else {
Service.Msg(res.msg)
}
})
};
const closePasswordPopup = () => {
showPasswordPopup.value = false;
passwordInput.value = '';
};
const goToSetPassword = () => {
closePasswordPopup();
Service.GoPage('/pages/userFunc/setPaypwd');
};
</script>
<style scoped lang="scss">
page {
background: linear-gradient(180deg, #FFF7ED 0%, #FFF5EB 50%, #F5F5F5 100%);
}
.transfer-section {
padding: 30rpx 30rpx 0;
}
.transfer-card {
background: #fff;
border-radius: 24rpx;
padding: 40rpx 30rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.06);
}
.form-item {
margin-bottom: 40rpx;
&:last-child {
margin-bottom: 30rpx;
}
}
.form-label {
display: block;
font-size: 28rpx;
color: #333;
font-weight: 500;
margin-bottom: 20rpx;
}
.input-wrapper {
display: flex;
align-items: center;
background-color: #F9FAFB;
border-radius: 16rpx;
padding: 24rpx 30rpx;
border: 2rpx solid transparent;
transition: all 0.3s;
&:focus-within {
border-color: #FF6A00;
background-color: #FFF;
}
&.amount-wrapper {
padding: 30rpx;
}
}
.currency-symbol {
font-size: 48rpx;
font-weight: bold;
color: #FF6A00;
margin-right: 10rpx;
line-height: 1;
}
.input-field {
flex: 1;
margin-left: 20rpx;
font-size: 30rpx;
color: #1F2937;
&.amount-input {
font-size: 48rpx;
font-weight: bold;
color: #1F2937;
margin-left: 0;
}
}
.type-options {
display: flex;
flex-direction: column;
gap: 20rpx;
}
.type-item {
display: flex;
align-items: center;
padding: 24rpx 30rpx;
background: #F9FAFB;
border: 2rpx solid transparent;
border-radius: 16rpx;
transition: all 0.3s;
&.active {
border-color: #FF6A00;
background: #FFF7ED;
}
&:active {
opacity: 0.8;
}
}
.type-radio {
width: 36rpx;
height: 36rpx;
border: 2rpx solid #ccc;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-right: 20rpx;
flex-shrink: 0;
.type-item.active & {
border-color: #FF6A00;
}
}
.type-radio-dot {
width: 20rpx;
height: 20rpx;
background: #FF6A00;
border-radius: 50%;
}
.type-info {
flex: 1;
}
.type-name {
font-size: 28rpx;
color: #333;
font-weight: 500;
}
.type-desc {
font-size: 22rpx;
color: #999;
margin-top: 4rpx;
}
.amount-hint {
display: flex;
align-items: center;
margin-top: 16rpx;
}
.hint-text {
font-size: 24rpx;
color: #999;
}
.available-amount {
font-size: 24rpx;
color: #FF6A00;
font-weight: 600;
}
.bottom-action {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: #fff;
padding: 20rpx 30rpx;
box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.06);
z-index: 100;
}
.transfer-btn {
width: 100%;
background: linear-gradient(135deg, #FF6A00 0%, #FF7F00 100%);
color: #fff;
font-size: 34rpx;
font-weight: 600;
border: none;
border-radius: 50rpx;
box-shadow: 0 8rpx 24rpx rgba(255, 106, 0, 0.3);
transition: all 0.3s;
&:disabled {
opacity: 0.5;
box-shadow: none;
}
&:active:not(:disabled) {
transform: scale(0.98);
box-shadow: 0 4rpx 12rpx rgba(255, 106, 0, 0.2);
}
}
.password-popup {
width: 600rpx;
background: #fff;
border-radius: 24rpx;
overflow: hidden;
}
.popup-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 30rpx;
border-bottom: 1rpx solid #F0F0F0;
}
.popup-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
}
.popup-close {
width: 50rpx;
height: 50rpx;
display: flex;
align-items: center;
justify-content: center;
}
.popup-content {
padding: 40rpx 30rpx 30rpx;
}
.password-desc {
text-align: center;
font-size: 28rpx;
color: #666;
margin-bottom: 40rpx;
}
.password-input-wrapper {
display: flex;
justify-content: center;
gap: 16rpx;
margin-bottom: 30rpx;
}
.password-box {
width: 80rpx;
height: 80rpx;
border: 2rpx solid #E5E7EB;
border-radius: 12rpx;
display: flex;
align-items: center;
justify-content: center;
background: #F9FAFB;
transition: all 0.3s;
&.active {
border-color: #FF6A00;
background: #FFF;
}
}
.password-dot {
width: 20rpx;
height: 20rpx;
background: #333;
border-radius: 50%;
}
.hidden-password-input {
position: absolute;
top: -9999rpx;
left: -9999rpx;
width: 1rpx;
height: 1rpx;
opacity: 0;
}
.forget-password {
text-align: center;
}
.forget-text {
font-size: 26rpx;
color: #FF6A00;
}
</style>

View File

@@ -0,0 +1,256 @@
<template>
<view class="wallet-page">
<!-- 账户列表 -->
<view class="accounts-section">
<view class="accounts-list">
<!-- 余额 -->
<view @click="goToDetail('account')" class="account-item">
<view class="account-left">
<view class="account-icon balance-icon">
<image :src="Service.GetIconImg('/static/icon/userFunc/wallet/01.png')" class="account-icon-img" />
</view>
<view class="account-text">
<text class="account-name">余额</text>
<text class="account-desc">账户余额</text>
</view>
</view>
<view class="account-right">
<text class="account-value">¥{{ accInfo.account || '0.00' }}</text>
<up-icon name="arrow-right" color="#C4C4C4" size="16"></up-icon>
</view>
</view>
<!-- 报单商品积分 -->
<view @click="goToDetail('customs')" class="account-item">
<view class="account-left">
<view class="account-icon customs-icon">
<image :src="Service.GetIconImg('/static/icon/userFunc/wallet/02.png')" class="account-icon-img" />
</view>
<view class="account-text">
<text class="account-name">报单积分</text>
<text class="account-desc">报单商品奖励</text>
</view>
</view>
<view class="account-right">
<text class="account-value">{{ accInfo.customs || '0' }}</text>
<up-icon name="arrow-right" color="#C4C4C4" size="16"></up-icon>
</view>
</view>
<!-- 普通商品积分 -->
<view @click="goToDetail('integral')" class="account-item">
<view class="account-left">
<view class="account-icon integral-icon">
<image :src="Service.GetIconImg('/static/icon/userFunc/wallet/03.png')" class="account-icon-img" />
</view>
<view class="account-text">
<text class="account-name">普通积分</text>
<text class="account-desc">普通商品奖励</text>
</view>
</view>
<view class="account-right">
<text class="account-value">{{ accInfo.integral || '0' }}</text>
<up-icon name="arrow-right" color="#C4C4C4" size="16"></up-icon>
</view>
</view>
<!-- 自营商品积分 -->
<view @click="goToDetail('selfIntegral')" class="account-item">
<view class="account-left">
<view class="account-icon self-icon">
<image :src="Service.GetIconImg('/static/icon/userFunc/wallet/04.png')" class="account-icon-img" />
</view>
<view class="account-text">
<text class="account-name">自营积分</text>
<text class="account-desc">自营商品奖励</text>
</view>
</view>
<view class="account-right">
<text class="account-value">{{ accInfo.selfIntegral || '0' }}</text>
<up-icon name="arrow-right" color="#C4C4C4" size="16"></up-icon>
</view>
</view>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { onShow } from '@dcloudio/uni-app';
import { Service } from '@/Service/Service';
import { UserService } from '@/Service/api/UserService';
// ==================== 自定义导航栏 ====================
let statusBarHeight = ref(uni.getWindowInfo().statusBarHeight || 20)
const accInfo = ref<any>({});
onShow(() => {
getAccInfo()
})
const getAccInfo = () => {
UserService.GetUserAccInfo().then(res => {
if (res.code == 0) {
accInfo.value = res.data.accInfo;
} else {
Service.Msg(res.msg);
}
});
};
const goToDetail = (type : string) => {
Service.GoPage('/pages/userFunc/walletRecord?type='+type)
};
</script>
<style scoped lang="scss">
.wallet-page {
background-color: #F8F9FA;
}
/* 账户区域 */
.accounts-section {
background-color: #fff;
margin: 30rpx 24rpx 24rpx;
border-radius: 24rpx;
padding: 32rpx 24rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.04);
}
.accounts-list {
display: flex;
flex-direction: column;
}
.account-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 28rpx 0;
}
.account-left {
display: flex;
align-items: center;
flex: 1;
}
.account-icon {
width: 88rpx;
height: 88rpx;
border-radius: 20rpx;
display: flex;
align-items: center;
justify-content: center;
margin-right: 20rpx;
}
.account-icon-img {
width: 50rpx;
height: 50rpx;
}
.balance-icon {
background: linear-gradient(135deg, #F97316, #FB923C);
}
.customs-icon {
background: linear-gradient(135deg, #10B981, #34D399);
}
.integral-icon {
background: linear-gradient(135deg, #8B5CF6, #A78BFA);
}
.self-icon {
background: linear-gradient(135deg, #F59E0B, #FBBF24);
}
.account-text {
display: flex;
flex-direction: column;
}
.account-name {
font-size: 30rpx;
color: #1A1A1A;
font-weight: 600;
margin-bottom: 6rpx;
}
.account-desc {
font-size: 24rpx;
color: #999;
}
.account-right {
display: flex;
align-items: center;
gap: 16rpx;
}
.account-value {
font-size: 32rpx;
color: #1A1A1A;
font-weight: 700;
}
// ==================== 自定义导航栏 ====================
.custom-nav {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 999;
background-color: #fff;
}
.nav-content {
height: 44px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 16rpx;
}
.nav-left {
width: 80rpx;
height: 44px;
display: flex;
align-items: center;
justify-content: center;
}
.nav-center {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
}
.nav-title {
font-size: 32rpx;
font-weight: 600;
color: #333;
}
.nav-right {
width: 80rpx;
}
.nav-placeholder {
width: 100%;
}
</style>

View File

@@ -0,0 +1,570 @@
<template>
<view class="balance-page">
<!-- 顶部余额区域 -->
<view class="balance-header">
<view class="header-bg"></view>
<view class="balance-info">
<text class="balance-label">
{{ getName(type) }} </text>
<view class="balance-value-wrapper">
<text v-if="type=='account'" class="currency">¥</text>
<text class="balance-value">{{ getAmount(type) }}</text>
</view>
<view class="balance-actions" v-if="type=='account'">
<view class="action-btn" @click="goWithdraw">
<text class="action-text">提现</text>
</view>
<view class="action-btn" @click="goTransfer">
<text class="action-text">转账</text>
</view>
</view>
</view>
</view>
<!-- 提现记录入口 -->
<view v-if="type=='account'" class="entry-row" @click="goWithdrawList">
<text class="entry-label">提现记录</text>
<text class="entry-arrow"></text>
</view>
<!-- 余额明细 -->
<view class="detail-section">
<view class="section-header">
<text class="section-title">明细</text>
<view class="filter-tabs">
<text class="filter-tab" v-for="(tab, index) in filterTabs" :key="index"
:class="{ active: currentFilter === index }" @click="switchFilter(index)">{{ tab.name }}</text>
</view>
</view>
<view class="detail-list">
<view v-if="detailList.length>0" class="detail-item" v-for="(item, index) in detailList" :key="index">
<view class="detail-icon">
<image :src="Service.GetIconImg('/static/icon/userFunc/money.png')"
style="width: 100% ; height: 100%;" />
</view>
<view class="detail-info">
<text class="detail-title">{{ item.name }}</text>
<text class="detail-desc">{{ item.remark }}</text>
<text class="detail-time">{{ Service.formatDate(item.addTime,1) }}</text>
</view>
<view class="detail-amount" :class="item.code === '收入' ? 'income' : 'expense'">
{{ item.code === '收入' ? '+' : '-' }} <text v-if="type=='account'">¥</text> {{ item.amount }}
</view>
</view>
<view v-else class=""
style="text-align: center; font-size: 32rpx; font-weight: bold; padding: 20rpx; border-radius: 10rpx; background-color: #f6f6f6; ">
暂无记录
</view>
</view>
</view>
<up-loadmore v-if="detailList.length>0" :status="status" />
</view>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { Service } from '@/Service/Service';
import { UserService } from "@/Service/api/UserService"
import { onLoad, onReachBottom, onShow } from '@dcloudio/uni-app';
const selectedPackage = ref(1);
const currentFilter = ref(0);
const filterTabs = ref([
{ name: '全部' },
{ name: '支出' },
{ name: '收入' }
]);
const detailList = ref<Array<any>>([
]);
let type = ref('account')
let accInfo = ref<any>({})
let page = ref(1)
let status = ref('nomore')
let shareAcc=ref(0)
onLoad((data : any) => {
type.value = data.type
if (data.type) {
uni.setNavigationBarTitle({
title: getTitile(data.type)
})
}
})
onShow(()=>{
getData()
getAccInfo()
})
onReachBottom(() => {
getList()
})
const getAmount = (name : any) => {
if (name == 'account') {
return accInfo.value.account
} else if (name == 'customs') {
return accInfo.value.customs
}
else if (name == 'integral') {
return accInfo.value.integral
} else if (name == 'selfIntegral') {
return accInfo.value.selfIntegral
}
}
const getTitile = (name : any) => {
if (name == 'account') {
return '余额明细'
} else if (name == 'customs') {
return '报单积分明细'
}else if (name == 'integral') {
return '普通积分明细'
}
else if (name == 'selfIntegral') {
return '自营积分明细'
}
}
const getName=(name:any)=>{
if (name == 'account') {
return '余额'
} else if (name == 'customs') {
return '报单积分'
}else if (name == 'integral') {
return '普通积分'
}
else if (name == 'selfIntegral') {
return '自营积分'
}
}
const getAccInfo = () => {
UserService.GetUserAccInfo().then(res => {
if (res.code == 0) {
accInfo.value = res.data.accInfo
shareAcc.value=res.data.shareAcc
} else {
Service.Msg(res.msg)
}
})
}
const getData = () => {
detailList.value = []
page.value = 1
status.value = 'loadmore'
getList()
}
const getList = () => {
if (status.value == 'loading' || status.value == 'nomore') {
return
}
status.value = 'loading'
UserService.GetUserAccLog(type.value, currentFilter.value == 0 ? '' : String(currentFilter.value - 1), page.value).then(res => {
if (res.code == 0) {
detailList.value = [...detailList.value, ...res.data.list]
status.value = res.data.list.length == 10 ? 'loadmore' : 'nomore'
page.value++
} else {
Service.Msg(res.msg)
}
})
}
const switchFilter = (index : number) => {
currentFilter.value = index;
getData()
};
const goWithdraw = () => {
Service.GoPage('/pages/userFunc/withDrow')
}
const goTransfer = () => {
Service.GoPage('/pages/userFunc/transfer?accType=' + type.value)
}
const goWithdrawList = () => {
Service.GoPage('/pages/userFunc/withdrowList')
}
</script>
<style scoped lang="scss">
.balance-page {
background-color: #f5f5f5;
}
/* 顶部余额区域 */
.balance-header {
background: linear-gradient(135deg, #F97316, #FB923C);
padding: 40rpx 30rpx 60rpx;
position: relative;
overflow: hidden;
text-align: center;
}
.header-bg {
position: absolute;
top: -100rpx;
right: -100rpx;
width: 400rpx;
height: 400rpx;
background-color: rgba(255, 255, 255, 0.1);
border-radius: 50%;
}
.balance-info {
position: relative;
z-index: 1;
}
.balance-label {
font-size: 26rpx;
color: rgba(255, 255, 255, 0.9);
display: block;
margin-bottom: 10rpx;
}
.balance-value-wrapper {
display: flex;
align-items: baseline;
justify-content: center;
margin-bottom: 40rpx;
}
.currency {
font-size: 32rpx;
color: #fff;
margin-right: 4rpx;
}
.balance-value {
font-size: 72rpx;
font-weight: bold;
color: #fff;
}
.balance-actions {
display: flex;
justify-content: center;
gap: 30rpx;
}
.action-btn {
min-width: 160rpx;
padding: 16rpx 40rpx;
border-radius: 40rpx;
background-color: rgba(255, 255, 255, 0.25);
border: 2rpx solid rgba(255, 255, 255, 0.5);
text-align: center;
backdrop-filter: blur(10rpx);
transition: all 0.2s;
}
.action-btn:active {
background-color: rgba(255, 255, 255, 0.4);
transform: scale(0.95);
}
.action-text {
font-size: 26rpx;
font-weight: 500;
color: #fff;
letter-spacing: 4rpx;
}
/* 提现记录入口 */
.entry-row {
display: flex;
align-items: center;
justify-content: space-between;
margin: 20rpx 20rpx 0;
padding: 24rpx 30rpx;
background-color: #fff;
border-radius: 16rpx;
}
.entry-row:active {
background-color: #f5f5f5;
}
.entry-label {
font-size: 28rpx;
font-weight: 500;
color: #333;
}
.entry-arrow {
font-size: 36rpx;
color: #ccc;
font-weight: bold;
}
/* 通用区域样式 */
.recharge-section,
.detail-section,
.stats-section {
background-color: #fff;
margin: 20rpx;
border-radius: 20rpx;
padding: 30rpx;
}
.section-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30rpx;
}
.filter-tabs {
display: flex;
gap: 30rpx;
}
.filter-tab {
font-size: 26rpx;
color: #999;
padding: 8rpx 0;
position: relative;
&.active {
color: #F97316;
font-weight: 500;
&::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 4rpx;
background-color: #F97316;
border-radius: 2rpx;
}
}
}
/* 充值套餐 */
.recharge-packages {
display: flex;
flex-wrap: wrap;
margin: -10rpx;
margin-bottom: 30rpx;
}
.package-item {
width: calc(50% - 20rpx);
margin: 10rpx;
padding: 30rpx 20rpx;
border: 2rpx solid #e5e5e5;
border-radius: 16rpx;
position: relative;
box-sizing: border-box;
&.active {
border-color: #F97316;
background-color: #FFF7ED;
}
}
.package-tag {
position: absolute;
top: 0;
right: 0;
background: linear-gradient(135deg, #F97316, #FB923C);
color: #fff;
font-size: 20rpx;
padding: 4rpx 16rpx;
border-radius: 0 16rpx 0 16rpx;
}
.package-info {
display: flex;
flex-direction: column;
align-items: center;
}
.package-name {
font-size: 28rpx;
color: #333;
margin-bottom: 10rpx;
}
.package-price {
display: flex;
align-items: baseline;
margin-bottom: 8rpx;
}
.price {
font-size: 36rpx;
color: #F97316;
font-weight: bold;
}
.original-price {
font-size: 22rpx;
color: #999;
text-decoration: line-through;
margin-left: 8rpx;
}
.package-bonus {
font-size: 22rpx;
color: #F97316;
background-color: #FFF7ED;
padding: 4rpx 12rpx;
border-radius: 8rpx;
}
.package-check {
position: absolute;
top: 10rpx;
left: 10rpx;
width: 40rpx;
height: 40rpx;
background: linear-gradient(135deg, #F97316, #FB923C);
color: #fff;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 24rpx;
font-weight: bold;
}
.recharge-btn {
background: linear-gradient(135deg, #F97316, #FB923C);
color: #fff;
font-size: 32rpx;
font-weight: 500;
text-align: center;
padding: 24rpx;
border-radius: 30rpx;
}
/* 余额明细 */
.detail-list {
display: flex;
flex-direction: column;
gap: 20rpx;
}
.detail-item {
display: flex;
align-items: center;
padding: 20rpx;
background-color: #F9FAFB;
border-radius: 12rpx;
}
.detail-icon {
width: 70rpx;
height: 70rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-right: 20rpx;
}
.icon-text {
font-size: 40rpx;
}
.detail-info {
flex: 1;
display: flex;
flex-direction: column;
}
.detail-title {
font-size: 28rpx;
color: #333;
font-weight: 500;
margin-bottom: 6rpx;
}
.detail-desc {
font-size: 22rpx;
color: #999;
margin-bottom: 4rpx;
}
.detail-time {
font-size: 22rpx;
color: #999;
}
.detail-amount {
font-size: 32rpx;
font-weight: bold;
&.income {
color: #10B981;
}
&.expense {
color: #F97316;
}
}
/* 收支统计 */
.stats-cards {
display: flex;
gap: 20rpx;
}
.stat-card {
flex: 1;
padding: 30rpx;
border-radius: 16rpx;
display: flex;
flex-direction: column;
align-items: center;
&.income {
background: linear-gradient(135deg, #ECFDF5, #D1FAE5);
}
&.expense {
background: linear-gradient(135deg, #FFF7ED, #FFEDD5);
}
}
.stat-label {
font-size: 26rpx;
color: #666;
margin-bottom: 10rpx;
}
.stat-value {
font-size: 36rpx;
font-weight: bold;
color: #333;
}
</style>

View File

@@ -0,0 +1,54 @@
<template>
<view>
<web-view :src="h5url" @message="handleWebMessage"></web-view>
</view>
</template>
<script setup lang="ts">
import { onShow, onLoad } from "@dcloudio/uni-app";
import { ref } from "vue";
import { Service } from "@/Service/Service";
const pages = getCurrentPages()
const vw = ref(null)
let h5url = ref()
let url = ref('http://192.168.0.192:5174/#')
let address = ref('/pages/userFunc/orderDetail')
onLoad((data : any) => {
h5url.value = url.value + address.value
});
onShow(() => {
});
// 接收H5 postMessage消息
const handleWebMessage = (e : any) => {
const msg = e.detail.data[0]
console.log('收到H5消息', msg)
const { code, data } = msg
if (code == 'Back') {
Service.GoPageBack()
}
if (code === 'Ready') {
SendMsg()
}
};
// 向子元素传递的值
function SendMsg(data : any) {
vw.value = pages[pages.length - 1].$getAppWebview().children()[0]
const jsonStr = JSON.stringify({ code: 'init', data: data })
vw.value.evalJS(`GetTwEvent('${jsonStr}')`)
}
</script>
<style lang="scss">
</style>

View File

@@ -0,0 +1,491 @@
<template>
<view class="withdraw-page">
<view class="withdraw-container">
<view class="withdraw-header">
<text class="page-title">申请提现</text>
<text class="page-subtitle">请填写账户信息</text>
</view>
<view class="withdraw-form">
<!-- 姓名 -->
<view class="input-item">
<up-icon name="account" size="20" color="#999"></up-icon>
<input v-model="withdrawForm.name" class="input-field" type="text" placeholder="请输入姓名" />
</view>
<!-- 银行卡号 -->
<view class="input-item">
<image :src="Service.GetIconImg('/static/icon/userFunc/userBank.png')"
style="width: 30rpx; height: 30rpx;" />
<input v-model="withdrawForm.bankName" class="input-field" maxlength="19" placeholder="请输入所属银行" />
</view>
<!-- 银行卡号 -->
<view class="input-item">
<image :src="Service.GetIconImg('/static/icon/userFunc/bank.png')"
style="width: 30rpx; height: 30rpx;" />
<input v-model="withdrawForm.bankCard" class="input-field" type="number" maxlength="19"
placeholder="请输入银行卡号" />
</view>
<!-- 金额 -->
<view class="input-item">
<up-icon name="rmb-circle" size="20" color="#999"></up-icon>
<input v-model="withdrawForm.amount" class="input-field" type="text" placeholder="请输入提现金额" />
</view>
<!-- 余额显示 -->
<view class="balance-info">
<view class="balance-row">
<text class="balance-label">可提现余额</text>
<text class="balance-value">¥{{ currentBalance || '0.00' }}</text>
</view>
<!-- <view class="balance-row">
<text class="quota-label">可提现额度</text>
<text class="balance-value">¥{{ withdrawQuota || '0.00' }}</text>
</view> -->
</view>
</view>
<view class="button-group">
<button class="withdraw-btn" @click="handleWithdraw">
确认提现
</button>
</view>
</view>
<!-- 支付密码弹窗 -->
<up-popup :show="showPasswordPopup" mode="center" @close="closePasswordPopup" :closeable="true" :round="10"
:closeOnClickOverlay="true">
<view class="password-popup">
<view class="password-popup-header">
<text class="password-popup-title">输入支付密码</text>
</view>
<view class="password-popup-content">
<view class="pay-amount">
<text class="amount-label">提现金额</text>
<text class="amount-value">¥{{ withdrawForm.amount || '0.00' }}</text>
</view>
<view class="password-input-wrapper">
<view class="password-dots">
<view v-for="(dot, index) in 6" :key="index" class="password-dot"
:class="{ filled: index < payPassword.length }">
</view>
</view>
<input v-model="payPassword" class="hidden-password-input" type="number" maxlength="6"
:focus="showPasswordPopup" @input="handlePasswordInput" />
</view>
<view class="password-actions">
<text class="forgot-password" @click="handleForgotPassword">忘记支付密码</text>
</view>
</view>
<view class="password-popup-footer">
<button class="cancel-btn" @click="closePasswordPopup">取消</button>
<button class="confirm-btn" @click="confirmWithdraw"
:disabled="payPassword.length < 6">确认提现</button>
</view>
</view>
</up-popup>
</view>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { onLoad } from '@dcloudio/uni-app';
import { Service } from '@/Service/Service';
import { UserService } from '@/Service/api/UserService';
const withdrawForm = ref({
name: '',
bankCard: '',
amount: '',
bankName: ''
});
const accountType = ref('user');
const currentBalance = ref('0.00');
const withdrawQuota = ref('0.00');
const accInfo = ref<any>({});
const showPasswordPopup = ref(false);
const payPassword = ref('');
let accType = ref('')
onLoad((data : any) => {
if (data.accType) {
accType.value = data.accType;
}
getAccInfo();
});
const getAccInfo = () => {
UserService.GetUserAccInfo().then(res => {
if (res.code == 0) {
accInfo.value = res.data.accInfo;
currentBalance.value = accInfo.value.account;
} else {
Service.Msg(res.msg);
}
});
};
const handleWithdraw = () => {
if (!withdrawForm.value.name) {
Service.Msg('请输入姓名');
return;
}
if (!withdrawForm.value.bankName) {
Service.Msg('请输入所属银行');
return;
}
if (!withdrawForm.value.bankCard) {
Service.Msg('请输入银行卡号');
return;
}
if (!withdrawForm.value.amount) {
Service.Msg('请输入提现金额');
return;
}
const amount = parseFloat(withdrawForm.value.amount);
const balance = parseFloat(currentBalance.value);
const quota = parseFloat(withdrawQuota.value);
if (amount <= 0) {
Service.Msg('提现金额必须大于0');
return;
}
if (amount > balance) {
Service.Msg('提现金额不能超过可提现余额');
return;
}
showPasswordPopup.value = true;
payPassword.value = '';
};
const handleTransfer = () => {
Service.Msg('转账功能开发中');
};
const handlePasswordInput = () => {
};
const closePasswordPopup = () => {
showPasswordPopup.value = false;
payPassword.value = '';
};
const handleForgotPassword = () => {
Service.GoPage('/pages/userFunc/setPaypwd');
};
const confirmWithdraw = () => {
if (payPassword.value.length < 6) {
Service.Msg('请输入完整的支付密码');
return;
}
Service.LoadIng('提现申请中...');
let data={
amount:withdrawForm.value.amount,
name:withdrawForm.value.name,
account: withdrawForm.value.bankCard,
pwd:payPassword.value,
bankName:withdrawForm.value.bankName
}
UserService.UserWithAcc(data).then(res => {
if (res.code == 0) {
Service.LoadClose();
Service.Msg('提现申请已提交,请等待审核');
closePasswordPopup();
setTimeout(() => {
Service.GoPageBack();
}, 1500);
} else {
Service.Msg(res.msg)
}
})
};
</script>
<style scoped lang="scss">
page {
background: linear-gradient(135deg, #FFF7ED 0%, #FFEDD5 100%);
}
.withdraw-page {
padding: 40rpx;
}
.withdraw-container {
width: 100%;
background-color: #FFFFFF;
border-radius: 32rpx;
padding: 60rpx 40rpx;
box-shadow: 0 8rpx 32rpx rgba(249, 115, 22, 0.1);
}
.withdraw-header {
margin-bottom: 50rpx;
}
.page-title {
display: block;
font-size: 44rpx;
font-weight: bold;
color: #1F2937;
margin-bottom: 10rpx;
}
.page-subtitle {
font-size: 28rpx;
color: #6B7280;
}
.withdraw-form {
margin-bottom: 40rpx;
}
.input-item {
display: flex;
align-items: center;
background-color: #F9FAFB;
border-radius: 16rpx;
padding: 24rpx 30rpx;
margin-bottom: 28rpx;
border: 2rpx solid transparent;
transition: all 0.3s;
&:focus-within {
border-color: #F97316;
background-color: #FFF;
}
}
.input-field {
flex: 1;
margin-left: 20rpx;
font-size: 30rpx;
color: #1F2937;
}
.balance-info {
display: flex;
justify-content: space-around;
gap: 16rpx;
padding: 30rpx;
background: linear-gradient(135deg, #FFF7ED, #FFEDD5);
border-radius: 16rpx;
}
.balance-row {
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
}
.balance-label {
font-size: 28rpx;
color: #6B7280;
}
.balance-value {
font-size: 36rpx;
font-weight: bold;
color: #F97316;
}
.quota-label {
font-size: 28rpx;
color: #6B7280;
}
.quota-value {
font-size: 28rpx;
font-weight: 500;
color: #3B82F6;
}
.balance-all {
font-size: 26rpx;
color: #F97316;
padding: 8rpx 20rpx;
background-color: #FFF;
border-radius: 20rpx;
}
.button-group {
display: flex;
gap: 20rpx;
margin-top: 20rpx;
}
.transfer-btn {
flex: 1;
padding: 0 30rpx;
background: #fff;
color: #F97316;
font-size: 34rpx;
font-weight: 600;
border: 2rpx solid #F97316;
border-radius: 16rpx;
transition: all 0.3s;
&:active {
transform: scale(0.98);
background-color: #FFF7ED;
}
}
.withdraw-btn {
flex: 1;
padding: 0 30rpx;
background: linear-gradient(135deg, #F97316, #FB923C);
color: #FFFFFF;
font-size: 34rpx;
font-weight: 600;
border: none;
border-radius: 16rpx;
box-shadow: 0 8rpx 24rpx rgba(249, 115, 22, 0.3);
transition: all 0.3s;
&:active {
transform: scale(0.98);
box-shadow: 0 4rpx 12rpx rgba(249, 115, 22, 0.2);
}
}
/* 支付密码弹窗样式 */
.password-popup {
width: 600rpx;
background-color: #fff;
border-radius: 20rpx;
overflow: hidden;
}
.password-popup-header {
padding: 40rpx 30rpx 30rpx;
text-align: center;
border-bottom: 1rpx solid #f5f5f5;
}
.password-popup-title {
font-size: 32rpx;
font-weight: 500;
color: #333;
}
.password-popup-content {
padding: 40rpx 30rpx;
}
.pay-amount {
text-align: center;
margin-bottom: 40rpx;
}
.amount-label {
display: block;
font-size: 26rpx;
color: #666;
margin-bottom: 10rpx;
}
.amount-value {
font-size: 48rpx;
font-weight: bold;
color: #F97316;
}
.password-input-wrapper {
position: relative;
margin-bottom: 30rpx;
}
.password-dots {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20rpx 30rpx;
background-color: #f9fafb;
border-radius: 16rpx;
border: 2rpx solid #e5e7eb;
}
.password-dot {
width: 20rpx;
height: 20rpx;
border-radius: 50%;
background-color: transparent;
border: 2rpx solid #d1d5db;
transition: all 0.3s;
&.filled {
background-color: #333;
border-color: #333;
}
}
.hidden-password-input {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0;
}
.password-actions {
text-align: right;
}
.forgot-password {
font-size: 26rpx;
color: #F97316;
}
.password-popup-footer {
display: flex;
padding: 0 30rpx 30rpx;
gap: 20rpx;
}
.cancel-btn,
.confirm-btn {
flex: 1;
height: 80rpx;
border-radius: 40rpx;
font-size: 30rpx;
display: flex;
align-items: center;
justify-content: center;
border: none;
}
.cancel-btn {
background-color: #f5f5f5;
color: #666;
}
.confirm-btn {
background: linear-gradient(135deg, #F97316, #FB923C);
color: #fff;
&:disabled {
opacity: 0.5;
}
}
</style>

View File

@@ -0,0 +1,232 @@
<template>
<view class="withdraw-list-page">
<view class="withdraw-list">
<view class="list-header">
<text class="list-title">提现记录</text>
</view>
<view v-if="withdrawList.length > 0" class="withdraw-item" v-for="(item, index) in withdrawList"
:key="index">
<view class="item-left">
<view class="user-info">
<text class="user-name">{{ item.name }}</text>
</view>
<view class="account-info">
<text class="account-label">提现银行</text>
<text class="account-value">{{ item.bankName }}</text>
</view>
<view class="account-info">
<text class="account-label">提现账户</text>
<text class="account-value">{{ item.account }}</text>
</view>
<view class="time-info">
<text v-if="item.service" class="fee-info">手续费: ¥{{ item.service }}</text>
<text v-if="item.refuse" class="fee-info" style="color: red;">拒绝原因:{{ item.refuse }}</text>
<text class="withdraw-time">提现时间: {{ Service.formatDate(item.addTime, 1) }}</text>
</view>
</view>
<view class="item-right">
<text class="amount">¥{{ item.withAmount }}</text>
<view class="status-tag" :class="getStatusClass(item.state)">
{{ getStatusText(item.state) }}
</view>
</view>
</view>
<view v-else class="empty-state">
<text class="empty-text">暂无提现记录</text>
</view>
</view>
<up-loadmore v-if="withdrawList.length > 0" :status="status" />
</view>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { onLoad, onReachBottom } from '@dcloudio/uni-app';
import { Service } from '@/Service/Service';
import { UserService } from '@/Service/api/UserService';
const withdrawList = ref<Array<any>>([]);
let page = ref(1);
let status = ref('nomore');
let accType = ref('moneyAcc');
onLoad((data : any) => {
getData()
});
onReachBottom(() => {
getList();
});
const getData = () => {
withdrawList.value = [];
page.value = 1;
status.value = 'loadmore';
getList();
};
const getList = () => {
if (status.value == 'loading' || status.value == 'nomore') {
return;
}
status.value = 'loading';
UserService.GetUserWithList('', page.value).then(res => {
if (res.code == 0) {
withdrawList.value = [...withdrawList.value, ...res.data.list];
status.value = res.data.list.length == 10 ? 'loadmore' : 'nomore';
page.value++;
} else {
Service.Msg(res.msg);
}
});
};
const getStatusText = (state : number) => {
const stateMap : { [key : number] : string } = {
0: '审核中',
1: '已打款',
2: '拒绝提现',
};
return stateMap[state] || '未知';
};
const getStatusClass = (state : number) => {
const classMap : { [key : number] : string } = {
0: 'pending',
1: 'processing',
2: 'success',
3: 'failed'
};
return classMap[state] || '';
};
</script>
<style scoped lang="scss">
.withdraw-list-page {
min-height: 100vh;
background-color: #f5f5f5;
padding: 20rpx;
}
.list-header {
margin-bottom: 20rpx;
}
.list-title {
font-size: 30rpx;
font-weight: bold;
color: #333;
}
.withdraw-item {
background-color: #fff;
border-radius: 20rpx;
padding: 30rpx;
margin-bottom: 20rpx;
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.04);
}
.item-left {
flex: 1;
}
.user-info {
margin-bottom: 12rpx;
}
.user-name {
font-size: 30rpx;
color: #333;
font-weight: 500;
}
.account-info {
margin-bottom: 8rpx;
display: flex;
align-items: center;
}
.account-label {
font-size: 24rpx;
color: #999;
}
.account-value {
font-size: 24rpx;
color: #666;
font-weight: 500;
}
.time-info {
display: flex;
flex-direction: column;
gap: 4rpx;
}
.withdraw-time {
font-size: 24rpx;
color: #999;
}
.fee-info {
font-size: 24rpx;
color: #666;
}
.item-right {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 12rpx;
}
.amount {
font-size: 32rpx;
font-weight: bold;
color: #F97316;
}
.status-tag {
font-size: 22rpx;
padding: 6rpx 16rpx;
border-radius: 20rpx;
&.pending {
background-color: #FFF7ED;
color: #F97316;
}
&.processing {
background-color: #EFF6FF;
color: #3B82F6;
}
&.success {
background-color: #ECFDF5;
color: #10B981;
}
&.failed {
background-color: #FEF2F2;
color: #EF4444;
}
}
.empty-state {
background-color: #fff;
border-radius: 20rpx;
padding: 80rpx 30rpx;
text-align: center;
}
.empty-text {
font-size: 28rpx;
color: #999;
}
</style>

View File

@@ -0,0 +1,428 @@
<template>
<view>
<!-- 自定义导航栏 -->
<view v-if="isLoading" class="custom-nav" :style="{ paddingTop: statusBarHeight + 'px' }">
<view class="nav-content">
<view class="nav-left" @click="Service.GoPageBack()">
<up-icon name="arrow-left" size="20" color="#000000"></up-icon>
</view>
<view class="nav-center">
<text class="nav-title">商品详情</text>
</view>
<view class="nav-right"></view>
</view>
</view>
<view v-if="isLoading" class="nav-placeholder" :style="{ height: (statusBarHeight + 44) + 'px' }"></view>
<!-- 骨架屏区域 - 数据加载时显示 -->
<view v-if="isLoading" class="skeleton-detail-container">
<!-- 轮播图骨架 -->
<view class="skeleton-swiper">
<view class="skeleton-item skeleton-shimmer"></view>
<view class="skeleton-indicator">
<view class="skeleton-item"></view>
</view>
</view>
<!-- 商品信息骨架 -->
<view class="skeleton-goods-info">
<view class="skeleton-price-row">
<view class="skeleton-price skeleton-item skeleton-shimmer"></view>
<view class="skeleton-old-price skeleton-item skeleton-shimmer"></view>
</view>
<view class="skeleton-tag skeleton-item skeleton-shimmer"></view>
<view class="skeleton-name skeleton-item skeleton-shimmer"></view>
<view class="skeleton-name skeleton-item skeleton-shimmer short"></view>
</view>
<!-- 标签栏骨架 -->
<view class="skeleton-tabs">
<view class="skeleton-tab skeleton-item skeleton-shimmer"></view>
<view class="skeleton-tab skeleton-item skeleton-shimmer"></view>
</view>
<!-- 商品介绍骨架 -->
<view class="skeleton-intro">
<view class="skeleton-intro-title skeleton-item skeleton-shimmer"></view>
<view class="skeleton-intro-content">
<view class="skeleton-intro-img skeleton-item skeleton-shimmer"></view>
<view class="skeleton-intro-img skeleton-item skeleton-shimmer"></view>
</view>
</view>
<!-- 评价区域骨架 -->
<view class="skeleton-comment">
<view class="skeleton-comment-title">
<view class="skeleton-item skeleton-shimmer"></view>
</view>
<view v-for="i in 2" :key="i" class="skeleton-comment-item">
<view class="skeleton-comment-avatar skeleton-item skeleton-shimmer"></view>
<view class="skeleton-comment-content">
<view class="skeleton-comment-header">
<view class="skeleton-item skeleton-shimmer"></view>
<view class="skeleton-item skeleton-shimmer"></view>
</view>
<view class="skeleton-comment-text skeleton-item skeleton-shimmer"></view>
<view class="skeleton-comment-text skeleton-item skeleton-shimmer short"></view>
</view>
</view>
</view>
<!-- 底部占位 -->
<view class="skeleton-bottom-placeholder"></view>
</view>
<web-view v-show="!isLoading" :src="h5url" @message="handleWebMessage"></web-view>
</view>
</template>
<script setup lang="ts">
import { onShow, onLoad } from "@dcloudio/uni-app";
import { ref } from "vue";
import { Service } from "@/Service/Service";
let isLoading = ref(true)
let statusBarHeight = ref(uni.getWindowInfo().statusBarHeight || 20)
const pages = getCurrentPages()
const vw = ref(null)
let h5url = ref()
let url = ref('http://192.168.0.192:5173/#')
let address = ref('/pages/userFunc/goodsDetail')
let goodsId = ref('')
let content = ref<any>({})
onLoad((data : any) => {
goodsId.value = data.goodsId
h5url.value = url.value + address.value
});
onShow(() => {
});
// 接收H5 postMessage消息
const handleWebMessage = (e : any) => {
const msg = e.detail.data[0]
console.log('收到H5消息', msg)
let token = Service.GetUserToken()
content.value = {
goodsId: goodsId.value,
token: token
}
const { code, data } = msg
if (code == 'Back') {
Service.GoPageBack()
}
if (code === 'Ready') {
isLoading.value = false
SendMsg(content.value)
}
if (code === 'orderDetail') {
Service.GoPage('/pages/webView/orderDetail?orderId=' + data)
}
if (code === 'shop') {
setTimeout(() => {
Service.GoPageTab('/pages/index/shop')
}, 1000)
}
};
// 向子元素传递的值
function SendMsg(data : any) {
vw.value = pages[pages.length - 1].$getAppWebview().children()[0]
const jsonStr = JSON.stringify({ code: 'init', data: data })
vw.value.evalJS(`GetTwEvent('${jsonStr}')`)
}
</script>
<style lang="scss">
// ==================== 自定义导航栏 ====================
.custom-nav {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 999;
background-color: #FFFFFF;
border-bottom: 1rpx solid #E5E5E5; // 加上底部细线
}
.nav-content {
height: 44px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 8rpx; // 原生左右边距较小
}
.nav-left {
width: 44px;
height: 44px;
display: flex;
align-items: center;
justify-content: center;
}
.nav-title {
font-size: 34rpx; // 接近原生 17px
color: #000000;
}
.nav-center {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
}
.nav-right {
width: 44px; // 与左侧对称,保证标题严格居中
}
// ==================== 骨架屏样式 ====================
// 骨架屏容器
.skeleton-detail-container {
padding-bottom: 200rpx;
background-color: #f6f6f6;
min-height: 100vh;
}
// 骨架屏基础元素样式 - 渐变灰色占位块
.skeleton-item {
// 使用柔和的渐变背景,从浅灰到稍深的灰色
background: linear-gradient(135deg,
#f8f8f8 0%,
#f0f0f0 50%,
#e8e8e8 100%);
border-radius: 8rpx;
}
// ==================== 闪光动画效果 ====================
// 使用CSS动画实现骨架屏的闪光效果
.skeleton-shimmer {
position: relative;
overflow: hidden;
// 创建闪光层
&::after {
content: '';
position: absolute;
top: 0;
left: -100%; // 从左侧开始
width: 100%;
height: 100%;
// 使用多层渐变创造更丰富的闪光效果
background: linear-gradient(90deg,
transparent 0%,
rgba(255, 255, 255, 0.3) 20%, // 淡入
rgba(255, 255, 255, 0.6) 40%, // 最亮
rgba(255, 255, 255, 0.4) 60%, // 稍暗
rgba(255, 255, 255, 0.2) 80%, // 淡出
transparent 100%);
animation: skeleton-shimmer 2s infinite; // 循环播放动画,稍慢一点更自然
}
// 添加一个额外的渐变层,创造更深的视觉层次
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(180deg,
rgba(255, 255, 255, 0.1) 0%,
transparent 50%,
rgba(0, 0, 0, 0.03) 100%);
pointer-events: none; // 不影响点击
}
}
// 闪光动画关键帧
@keyframes skeleton-shimmer {
0% {
transform: translateX(-100%); // 起始位置
}
100% {
transform: translateX(100%); // 结束位置
}
}
// ==================== 轮播图骨架 ====================
.skeleton-swiper {
width: 100%;
height: 600rpx;
background-color: #fff;
position: relative;
.skeleton-item {
width: 100%;
height: 100%;
border-radius: 0;
}
}
.skeleton-indicator {
position: absolute;
bottom: 30rpx;
right: 30rpx;
.skeleton-item {
width: 80rpx;
height: 40rpx;
border-radius: 40rpx;
}
}
// ==================== 商品信息骨架 ====================
.skeleton-goods-info {
background-color: #fff;
padding: 20rpx;
margin-bottom: 20rpx;
}
.skeleton-price-row {
display: flex;
align-items: baseline;
margin-bottom: 20rpx;
}
.skeleton-price {
width: 160rpx;
height: 40rpx;
margin-right: 20rpx;
}
.skeleton-old-price {
width: 100rpx;
height: 28rpx;
}
.skeleton-tag {
width: 300rpx;
height: 36rpx;
margin-bottom: 20rpx;
}
.skeleton-name {
width: 100%;
height: 32rpx;
margin-bottom: 12rpx;
&.short {
width: 70%;
}
}
// ==================== 标签栏骨架 ====================
.skeleton-tabs {
background-color: #fff;
padding: 20rpx;
margin-bottom: 20rpx;
display: flex;
gap: 60rpx;
justify-content: center;
}
.skeleton-tab {
width: 120rpx;
height: 36rpx;
}
// ==================== 商品介绍骨架 ====================
.skeleton-intro {
background-color: #fff;
padding: 20rpx;
margin-bottom: 20rpx;
}
.skeleton-intro-title {
width: 160rpx;
height: 36rpx;
margin-bottom: 30rpx;
}
.skeleton-intro-content {
display: flex;
flex-direction: column;
gap: 20rpx;
}
.skeleton-intro-img {
width: 100%;
height: 400rpx;
}
// ==================== 评价区域骨架 ====================
.skeleton-comment {
background-color: #fff;
padding: 20rpx;
}
.skeleton-comment-title {
margin-bottom: 30rpx;
.skeleton-item {
width: 200rpx;
height: 36rpx;
}
}
.skeleton-comment-item {
display: flex;
padding: 20rpx 0;
border-top: 1rpx solid #e2e2e2;
}
.skeleton-comment-avatar {
width: 60rpx;
height: 60rpx;
border-radius: 50%;
flex-shrink: 0;
margin-right: 16rpx;
}
.skeleton-comment-content {
flex: 1;
}
.skeleton-comment-header {
display: flex;
justify-content: space-between;
margin-bottom: 20rpx;
.skeleton-item:first-child {
width: 120rpx;
height: 28rpx;
}
.skeleton-item:last-child {
width: 140rpx;
height: 24rpx;
}
}
.skeleton-comment-text {
width: 100%;
height: 28rpx;
margin-bottom: 12rpx;
&.short {
width: 60%;
}
}
// ==================== 底部占位 ====================
.skeleton-bottom-placeholder {
width: 100%;
height: 200rpx;
}
</style>

View File

@@ -0,0 +1,363 @@
<template>
<view>
<!-- 自定义导航栏 -->
<view v-if="isLoading" class="custom-nav" :style="{ paddingTop: statusBarHeight + 'px' }">
<view class="nav-content">
<view class="nav-left" @click="Service.GoPageBack()">
<up-icon name="arrow-left" size="20" color="#000000"></up-icon>
</view>
<view class="nav-center">
<text class="nav-title">订单详情</text>
</view>
<view class="nav-right"></view>
</view>
</view>
<view class="nav-placeholder" :style="{ height: (statusBarHeight + 44) + 'px' }"></view>
<template v-if="isLoading">
<!-- 骨架屏 -->
<view class="skeleton-wrapper">
<!-- 订单状态 -->
<view class="order-status skeleton-status">
<view class="skeleton-text-lg"></view>
<view class="skeleton-text-sm"></view>
</view>
<!-- 收货信息 -->
<view class="shipping-info skeleton-shipping">
<view class="skeleton-icon"></view>
<view class="skeleton-shipping-content">
<view class="skeleton-name-phone">
<view class="skeleton-text" style="width: 120rpx;"></view>
<view class="skeleton-text" style="width: 200rpx;"></view>
</view>
<view class="skeleton-text" style="width: 80%;"></view>
</view>
<view class="skeleton-icon-sm"></view>
</view>
<!-- 商品列表 -->
<view class="goods-list skeleton-goods-list">
<view class="goods-item skeleton-goods-item" v-for="i in 2" :key="i">
<view class="skeleton-img"></view>
<view class="skeleton-goods-info">
<view class="skeleton-text" style="width: 90%;"></view>
<view class="skeleton-text" style="width: 50%;"></view>
<view class="skeleton-price-quantity">
<view class="skeleton-text" style="width: 120rpx;"></view>
<view class="skeleton-text" style="width: 60rpx;"></view>
</view>
</view>
</view>
<view class="info-item skeleton-total">
<view class="skeleton-text" style="width: 140rpx;"></view>
<view class="skeleton-text" style="width: 160rpx;"></view>
</view>
</view>
<!-- 订单信息 -->
<view class="order-info skeleton-order-info">
<view class="section-title skeleton-title"></view>
<view class="info-item skeleton-info-item" v-for="i in 4" :key="i">
<view class="skeleton-text" style="width: 140rpx;"></view>
<view class="skeleton-text" style="width: 200rpx;"></view>
</view>
</view>
<!-- 底部占位 -->
<view class="skeleton-bottom-space"></view>
<!-- 底部按钮 -->
<view class="bottom-buttons skeleton-bottom">
<view class="skeleton-btn"></view>
</view>
</view>
</template>
<web-view v-show="!isLoading" :src="h5url" @message="handleWebMessage"></web-view>
</view>
</template>
<script setup lang="ts">
import { onShow, onLoad } from "@dcloudio/uni-app";
import { ref } from "vue";
import { Service } from "@/Service/Service";
let isLoading = ref(true)
let statusBarHeight = ref(uni.getWindowInfo().statusBarHeight || 20)
const pages = getCurrentPages()
const vw = ref(null)
let h5url = ref()
let url = ref('http://192.168.0.192:5173/#')
let address = ref('/pages/userFunc/orderDetail')
let orderId = ref(0)
let content = ref<any>({})
onLoad((data : any) => {
orderId.value=data.orderId
h5url.value = url.value + address.value
});
onShow(() => {
});
// 接收H5 postMessage消息
const handleWebMessage = (e : any) => {
const msg = e.detail.data[0]
console.log('收到H5消息', msg)
let token = Service.GetUserToken()
content.value = {
orderId: orderId.value,
token: token
}
const { code, data } = msg
if (code == 'Back') {
Service.GoPageBack()
}
if (code === 'Ready') {
SendMsg(content.value)
isLoading.value=false
}
if (code === 'addAddress') {
Service.GoPage('/pages/userFunc/addAddress');
}
};
// 向子元素传递的值
function SendMsg(data : any) {
vw.value = pages[pages.length - 1].$getAppWebview().children()[0]
const jsonStr = JSON.stringify({ code: 'init', data: data })
vw.value.evalJS(`GetTwEvent('${jsonStr}')`)
}
</script>
<style lang="scss">
// ==================== 自定义导航栏 ====================
.custom-nav {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 999;
background-color: #FFFFFF;
border-bottom: 1rpx solid #E5E5E5; // 加上底部细线
}
.nav-content {
height: 44px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 8rpx; // 原生左右边距较小
}
.nav-left {
width: 44px;
height: 44px;
display: flex;
align-items: center;
justify-content: center;
}
.nav-title {
font-size: 34rpx; // 接近原生 17px
color: #000000;
}
.nav-center {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
}
.nav-right {
width: 44px; // 与左侧对称,保证标题严格居中
}
// ==================== 骨架屏 ====================
@keyframes skeleton-loading {
0% { background-position: 100% 50%; }
100% { background-position: 0 50%; }
}
.skeleton-wrapper {
transition: opacity 0.3s ease;
.skeleton,
.skeleton-text,
.skeleton-text-lg,
.skeleton-text-sm,
.skeleton-icon,
.skeleton-icon-sm,
.skeleton-img,
.skeleton-btn,
.skeleton-title {
background: linear-gradient(90deg, #f2f2f2 25%, #e6e6e6 50%, #f2f2f2 75%);
background-size: 200% 100%;
animation: skeleton-loading 1.5s infinite;
border-radius: 8rpx;
}
.skeleton-text {
height: 28rpx;
}
.skeleton-text-lg {
height: 44rpx;
width: 160rpx;
margin-bottom: 16rpx;
}
.skeleton-text-sm {
height: 24rpx;
width: 240rpx;
}
.skeleton-icon {
width: 30rpx;
height: 30rpx;
border-radius: 50%;
}
.skeleton-icon-sm {
width: 20rpx;
height: 20rpx;
border-radius: 50%;
}
.skeleton-img {
width: 160rpx;
height: 160rpx;
border-radius: 10rpx;
margin-right: 20rpx;
flex-shrink: 0;
}
.skeleton-btn {
width: 100%;
height: 70rpx;
border-radius: 35rpx;
}
}
.skeleton-status {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background-color: #fff;
padding: 30rpx;
}
.skeleton-shipping {
display: flex;
align-items: center;
background-color: #fff;
margin-top: 20rpx;
padding: 30rpx;
.skeleton-shipping-content {
flex: 1;
margin-right: 20rpx;
.skeleton-name-phone {
display: flex;
justify-content: space-between;
margin-bottom: 16rpx;
}
}
}
.skeleton-goods-list {
background-color: #fff;
margin-top: 20rpx;
padding: 0 30rpx;
.skeleton-goods-item {
display: flex;
padding: 30rpx 0;
border-bottom: 1rpx solid #f5f5f5;
&:last-child {
border-bottom: none;
}
.skeleton-goods-info {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
.skeleton-text {
margin-bottom: 16rpx;
&:last-child {
margin-bottom: 0;
}
}
.skeleton-price-quantity {
display: flex;
justify-content: space-between;
align-items: center;
}
}
}
.skeleton-total {
display: flex;
justify-content: space-between;
padding: 20rpx 0;
}
}
.skeleton-order-info {
background-color: #fff;
margin-top: 20rpx;
padding: 30rpx;
.skeleton-title {
width: 140rpx;
height: 32rpx;
margin-bottom: 25rpx;
}
.skeleton-info-item {
display: flex;
justify-content: space-between;
padding: 18rpx 0;
}
}
.skeleton-bottom-space {
width: 100%;
height: 100rpx;
}
.skeleton-bottom {
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: 110rpx;
padding: 20rpx;
background-color: #fff;
border-top: 1rpx solid #eee;
display: flex;
align-items: center;
}
</style>

View File

@@ -0,0 +1,266 @@
<template>
<view>
<!-- 自定义导航栏 -->
<view v-if="isLoading" class="custom-nav" :style="{ paddingTop: statusBarHeight + 'px' }">
<view class="nav-content">
<view class="nav-left" @click="Service.GoPageBack()">
<up-icon name="arrow-left" size="20" color="#000000"></up-icon>
</view>
<view class="nav-center">
<text class="nav-title">我的订单</text>
</view>
<view class="nav-right"></view>
</view>
</view>
<view v-if="isLoading" class="nav-placeholder" :style="{ height: (statusBarHeight + 44) + 'px' }"></view>
<!-- 订单列表骨架屏 -->
<view class="skeleton-wrapper" v-if="isLoading">
<view class="skeleton-order-item" v-for="i in 3" :key="i">
<view class="skeleton-header">
<view class="skeleton-text" style="width: 260rpx;"></view>
<view class="skeleton-text" style="width: 140rpx;"></view>
</view>
<view class="skeleton-product">
<view class="skeleton-img"></view>
<view class="skeleton-product-details">
<view class="skeleton-text" style="width: 90%;"></view>
<view class="skeleton-text" style="width: 60%;"></view>
<view class="skeleton-text" style="width: 120rpx;"></view>
</view>
</view>
<view class="skeleton-total">
<view class="skeleton-text" style="width: 160rpx;"></view>
</view>
<view class="skeleton-footer">
<view class="skeleton-status"></view>
<view class="skeleton-actions">
<view class="skeleton-btn secondary"></view>
<view class="skeleton-btn primary"></view>
</view>
</view>
</view>
</view>
<web-view v-show="!isLoading" :src="h5url" @message="handleWebMessage"></web-view>
</view>
</template>
<script setup lang="ts">
import { onShow, onLoad } from "@dcloudio/uni-app";
import { ref } from "vue";
import { Service } from "@/Service/Service";
let isLoading = ref(true)
let statusBarHeight = ref(uni.getWindowInfo().statusBarHeight || 20)
const pages = getCurrentPages()
const vw = ref(null)
let h5url = ref()
let url = ref('http://192.168.0.192:5173/#')
let address = ref('/pages/userFunc/orderList')
let type = ref(0)
let content = ref<any>({})
onLoad((data : any) => {
console.log(data);
type.value = data.type
h5url.value = url.value + address.value
});
onShow(() => {
});
// 接收H5 postMessage消息
const handleWebMessage = (e : any) => {
const msg = e.detail.data[0]
console.log('收到H5消息', msg)
let token = Service.GetUserToken()
content.value = {
type: type.value,
token: token
}
const { code, data } = msg
if (code == 'Back') {
Service.GoPageBack()
}
if (code === 'Ready') {
SendMsg(content.value)
isLoading.value=false
}
if (code === 'orderDetail') {
Service.GoPage('/pages/webView/orderDetail?orderId=' + data)
}
if (code === 'comment') {
Service.GoPage('/pages/userFunc/goComment?orderId=' + data)
}
};
// 向子元素传递的值
function SendMsg(data : any) {
vw.value = pages[pages.length - 1].$getAppWebview().children()[0]
const jsonStr = JSON.stringify({ code: 'init', data: data })
vw.value.evalJS(`GetTwEvent('${jsonStr}')`)
}
</script>
<style lang="scss">
// ==================== 骨架屏 ====================
@keyframes skeleton-loading {
0% { background-position: 100% 50%; }
100% { background-position: 0 50%; }
}
.skeleton-wrapper {
padding: 20rpx;
transition: opacity 0.3s ease;
.skeleton,
.skeleton-text,
.skeleton-img,
.skeleton-status,
.skeleton-btn {
background: linear-gradient(90deg, #f2f2f2 25%, #e6e6e6 50%, #f2f2f2 75%);
background-size: 200% 100%;
animation: skeleton-loading 1.5s infinite;
border-radius: 8rpx;
}
.skeleton-text {
height: 28rpx;
}
.skeleton-img {
width: 180rpx;
height: 180rpx;
border-radius: 16rpx;
margin-right: 20rpx;
flex-shrink: 0;
}
.skeleton-status {
width: 100rpx;
height: 40rpx;
border-radius: 10rpx;
}
.skeleton-btn {
height: 56rpx;
border-radius: 40rpx;
&.secondary {
width: 140rpx;
}
&.primary {
width: 140rpx;
}
}
}
.skeleton-order-item {
background-color: #fff;
border-radius: 20rpx;
padding: 30rpx;
margin-bottom: 20rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.04);
.skeleton-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
}
.skeleton-product {
display: flex;
padding-bottom: 20rpx;
border-bottom: 1rpx solid #e2e2e2;
.skeleton-product-details {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
.skeleton-text {
margin-bottom: 16rpx;
&:last-child {
margin-bottom: 0;
}
}
}
}
.skeleton-total {
display: flex;
justify-content: flex-end;
padding: 20rpx 0;
}
.skeleton-footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 20rpx;
.skeleton-actions {
display: flex;
gap: 16rpx;
}
}
}
// ==================== 自定义导航栏 ====================
.custom-nav {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 999;
background-color: #FFFFFF;
border-bottom: 1rpx solid #E5E5E5; // 加上底部细线
}
.nav-content {
height: 44px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 8rpx; // 原生左右边距较小
}
.nav-left {
width: 44px;
height: 44px;
display: flex;
align-items: center;
justify-content: center;
}
.nav-title {
font-size: 34rpx; // 接近原生 17px
color: #000000;
}
.nav-center {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
}
.nav-right {
width: 44px; // 与左侧对称,保证标题严格居中
}
</style>

6
src/shime-uni.d.ts vendored Normal file
View File

@@ -0,0 +1,6 @@
export {}
declare module "vue" {
type Hooks = App.AppInstance & Page.PageInstance;
interface ComponentCustomOptions extends Hooks {}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 377 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 583 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 570 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 693 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 418 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 790 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 437 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 434 B

BIN
src/static/goods/left.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 314 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 776 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 449 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 184 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 516 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 275 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 482 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 523 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 489 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 436 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 262 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 735 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 655 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 841 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 734 B

BIN
src/static/icon/my/get.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 515 B

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