9.17/1
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
## 前后端数据库状态说明
|
||||
|
||||
**更新日期**: 2025-09-16
|
||||
**更新日期**: 2025-09-17
|
||||
|
||||
### 概要
|
||||
- 数据库已落地:已在远程 MySQL `mysql.tonaspace.com` 的 `partsinquiry` 库完成初始化(表结构与触发器已创建)。
|
||||
@@ -27,12 +27,15 @@
|
||||
- 关闭:不设置/置为 `false` 即可停用(生产环境默认关闭)。
|
||||
- 完全移除:删除 `frontend/common/config.js` 中默认用户配置与 `frontend/common/http.js` 中注入逻辑。
|
||||
|
||||
### 后端(Spring Boot)数据库状态
|
||||
- 依赖:`pom.xml` 未包含 `spring-boot-starter-web`、`spring-boot-starter-data-jpa`、`mysql-connector-j` 等数据库相关依赖。
|
||||
- 配置:`src/main/resources/application.properties` 仅有 `spring.application.name=demo`;未配置 `spring.datasource.*`、`spring.jpa.*`。
|
||||
- 数据模型:`src/main/java` 未发现 `@Entity`、Repository、Service;存在 `backend/db/db.sql` 脚本,已执行至远程库。
|
||||
- 迁移:未发现 Flyway/Liquibase 配置与脚本(当前通过 MysqlMCP 手工执行)。
|
||||
- 结论:数据库已初始化,但后端未配置运行时数据源与接口,暂不可用。
|
||||
### 后端(Spring Boot)状态
|
||||
- 依赖:`pom.xml` 已包含 `spring-boot-starter-web`、`spring-boot-starter-data-jpa`、`mysql-connector-j`。
|
||||
- 配置:`application.properties` 使用环境变量注入数据源,已补充 Hikari/JPA;新增附件占位图配置:
|
||||
- `attachments.placeholder.image-path`(env: `ATTACHMENTS_PLACEHOLDER_IMAGE`)
|
||||
- `attachments.placeholder.url-path`(env: `ATTACHMENTS_PLACEHOLDER_URL`,默认 `/api/attachments/placeholder`)
|
||||
- 接口:新增附件相关接口(占位方案):
|
||||
- POST `/api/attachments`:忽略内容,返回 `{ url: "/api/attachments/placeholder" }`
|
||||
- GET `/api/attachments/placeholder`:返回本地占位图二进制
|
||||
- 迁移:仍建议引入 Flyway/Liquibase;结构变更继续通过 MysqlMCP 并同步 `/doc/database_documentation.md`。
|
||||
|
||||
### 前端(uni-app)数据库状态
|
||||
- 数据持久化:未见 IndexedDB/WebSQL/SQLite/云数据库使用;页面数据为内置静态数据。
|
||||
@@ -49,5 +52,26 @@
|
||||
- 引入迁移工具(Flyway/Liquibase)管理 DDL;后续所有变更继续通过 MysqlMCP 执行,并同步 `/doc/database_documentation.md`。
|
||||
- 增加健康检查与基础 CRUD 接口;在 `/doc/openapi.yaml` 按规范登记并标注实现状态(❌/✅)。
|
||||
|
||||
### 前端默认连接策略
|
||||
- 默认后端地址:`http://192.168.31.193:8080`(可被环境变量/Storage 覆盖)
|
||||
- 多地址重试:按顺序尝试(去重处理):`[ENV, Storage, 192.168.31.193:8080, 127.0.0.1:8080, localhost:8080]`
|
||||
- 默认用户:开启(可被环境变量/Storage 关闭),请求自动附带 `X-User-Id`(默认 `2`)。
|
||||
- 如需关闭:在 Storage 或构建环境中设置 `ENABLE_DEFAULT_USER=false`。
|
||||
|
||||
|
||||
### 占位图策略(当前阶段)
|
||||
- 说明:所有图片上传与展示均统一使用占位图,实际文件存储暂不开发。
|
||||
- 本地占位图:`C:\Users\21826\Desktop\Wj\PartsInquiry\backend\picture\屏幕截图 2025-08-14 134657.png`
|
||||
- 配置方式:
|
||||
- PowerShell(当前用户持久化)
|
||||
```powershell
|
||||
setx ATTACHMENTS_PLACEHOLDER_IMAGE "C:\\Users\\21826\\Desktop\\Wj\\PartsInquiry\\backend\\picture\\屏幕截图 2025-08-14 134657.png"
|
||||
setx ATTACHMENTS_PLACEHOLDER_URL "/api/attachments/placeholder"
|
||||
```
|
||||
- 应用重启后生效;也可在运行环境变量中注入。
|
||||
- 前端影响:
|
||||
- `components/ImageUploader.vue` 上传始终得到 `{ url: '/api/attachments/placeholder' }`
|
||||
- 商品列表/详情展示该占位图地址
|
||||
|
||||
|
||||
|
||||
|
||||
BIN
backend/picture/屏幕截图 2025-08-14 134657.png
Normal file
BIN
backend/picture/屏幕截图 2025-08-14 134657.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
@@ -0,0 +1,73 @@
|
||||
package com.example.demo.attachment;
|
||||
|
||||
import org.springframework.core.io.FileSystemResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.http.CacheControl;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/attachments")
|
||||
public class AttachmentController {
|
||||
|
||||
private final AttachmentPlaceholderProperties placeholderProperties;
|
||||
|
||||
public AttachmentController(AttachmentPlaceholderProperties placeholderProperties) {
|
||||
this.placeholderProperties = placeholderProperties;
|
||||
}
|
||||
|
||||
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
public ResponseEntity<Map<String, Object>> upload(@RequestParam("file") MultipartFile file,
|
||||
@RequestParam(value = "ownerType", required = false) String ownerType,
|
||||
@RequestParam(value = "ownerId", required = false) String ownerId) {
|
||||
// 占位实现:忽略文件内容,始终返回占位图 URL
|
||||
String url = StringUtils.hasText(placeholderProperties.getUrlPath()) ? placeholderProperties.getUrlPath() : "/api/attachments/placeholder";
|
||||
Map<String, Object> body = new HashMap<>();
|
||||
body.put("url", url);
|
||||
return ResponseEntity.ok(body);
|
||||
}
|
||||
|
||||
@GetMapping("/placeholder")
|
||||
public ResponseEntity<Resource> placeholder() throws IOException {
|
||||
String imagePath = placeholderProperties.getImagePath();
|
||||
if (!StringUtils.hasText(imagePath)) {
|
||||
return ResponseEntity.status(404).build();
|
||||
}
|
||||
Path path = Path.of(imagePath);
|
||||
if (!Files.exists(path)) {
|
||||
return ResponseEntity.status(404).build();
|
||||
}
|
||||
Resource resource = new FileSystemResource(path);
|
||||
String contentType = Files.probeContentType(path);
|
||||
MediaType mediaType;
|
||||
try {
|
||||
mediaType = StringUtils.hasText(contentType) ? MediaType.parseMediaType(contentType) : MediaType.IMAGE_PNG;
|
||||
} catch (Exception e) {
|
||||
mediaType = MediaType.APPLICATION_OCTET_STREAM;
|
||||
}
|
||||
return ResponseEntity.ok()
|
||||
.header(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=placeholder")
|
||||
.cacheControl(CacheControl.maxAge(30, TimeUnit.DAYS).cachePublic())
|
||||
.contentType(mediaType)
|
||||
.body(resource);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.example.demo.attachment;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
@ConfigurationProperties(prefix = "attachments.placeholder")
|
||||
public class AttachmentPlaceholderProperties {
|
||||
|
||||
private String imagePath;
|
||||
private String urlPath = "/api/attachments/placeholder";
|
||||
|
||||
public String getImagePath() {
|
||||
return imagePath;
|
||||
}
|
||||
|
||||
public void setImagePath(String imagePath) {
|
||||
this.imagePath = imagePath;
|
||||
}
|
||||
|
||||
public String getUrlPath() {
|
||||
return urlPath;
|
||||
}
|
||||
|
||||
public void setUrlPath(String urlPath) {
|
||||
this.urlPath = urlPath;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.example.demo.common;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
@ConfigurationProperties(prefix = "app.defaults")
|
||||
public class AppDefaultsProperties {
|
||||
|
||||
private Long shopId = 1L;
|
||||
private Long userId = 2L;
|
||||
|
||||
public Long getShopId() { return shopId; }
|
||||
public void setShopId(Long shopId) { this.shopId = shopId; }
|
||||
public Long getUserId() { return userId; }
|
||||
public void setUserId(Long userId) { this.userId = userId; }
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -17,9 +17,11 @@ public class DashboardController {
|
||||
}
|
||||
|
||||
@GetMapping("/overview")
|
||||
public ResponseEntity<DashboardOverviewResponse> overview(@RequestHeader(name = "X-Shop-Id", required = false) Long shopId) {
|
||||
long sid = (shopId == null ? 1L : shopId);
|
||||
return ResponseEntity.ok(dashboardService.getOverview(sid));
|
||||
public ResponseEntity<DashboardOverviewResponse> overview(
|
||||
@RequestHeader(name = "X-Shop-Id", required = false) Long shopId,
|
||||
@RequestHeader(name = "X-User-Id", required = false) Long userId
|
||||
) {
|
||||
return ResponseEntity.ok(dashboardService.getOverviewByUserOrShop(userId, shopId));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,11 +4,13 @@ import java.math.BigDecimal;
|
||||
|
||||
public class DashboardOverviewResponse {
|
||||
private BigDecimal todaySalesAmount;
|
||||
private BigDecimal monthSalesAmount;
|
||||
private BigDecimal monthGrossProfit;
|
||||
private BigDecimal stockTotalQuantity;
|
||||
|
||||
public DashboardOverviewResponse(BigDecimal todaySalesAmount, BigDecimal monthGrossProfit, BigDecimal stockTotalQuantity) {
|
||||
public DashboardOverviewResponse(BigDecimal todaySalesAmount, BigDecimal monthSalesAmount, BigDecimal monthGrossProfit, BigDecimal stockTotalQuantity) {
|
||||
this.todaySalesAmount = todaySalesAmount;
|
||||
this.monthSalesAmount = monthSalesAmount;
|
||||
this.monthGrossProfit = monthGrossProfit;
|
||||
this.stockTotalQuantity = stockTotalQuantity;
|
||||
}
|
||||
@@ -24,6 +26,10 @@ public class DashboardOverviewResponse {
|
||||
public BigDecimal getStockTotalQuantity() {
|
||||
return stockTotalQuantity;
|
||||
}
|
||||
|
||||
public BigDecimal getMonthSalesAmount() {
|
||||
return monthSalesAmount;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -33,6 +33,16 @@ public class DashboardRepository {
|
||||
return toBigDecimal(result);
|
||||
}
|
||||
|
||||
public BigDecimal sumMonthSalesOrders(Long shopId) {
|
||||
Object result = entityManager.createNativeQuery(
|
||||
"SELECT COALESCE(SUM(amount), 0) FROM sales_orders " +
|
||||
"WHERE shop_id = :shopId AND status = 'approved' " +
|
||||
"AND order_time >= DATE_FORMAT(CURRENT_DATE(), '%Y-%m-01') " +
|
||||
"AND order_time < DATE_ADD(DATE_FORMAT(CURRENT_DATE(), '%Y-%m-01'), INTERVAL 1 MONTH)"
|
||||
).setParameter("shopId", shopId).getSingleResult();
|
||||
return toBigDecimal(result);
|
||||
}
|
||||
|
||||
public BigDecimal sumTotalInventoryQty(Long shopId) {
|
||||
Object result = entityManager.createNativeQuery(
|
||||
"SELECT COALESCE(SUM(quantity), 0) FROM inventories WHERE shop_id = :shopId"
|
||||
@@ -50,6 +60,20 @@ public class DashboardRepository {
|
||||
return BigDecimal.ZERO;
|
||||
}
|
||||
}
|
||||
|
||||
public Long findShopIdByUserId(Long userId) {
|
||||
if (userId == null) return null;
|
||||
Object result = entityManager.createNativeQuery(
|
||||
"SELECT shop_id FROM users WHERE id = :userId LIMIT 1"
|
||||
).setParameter("userId", userId).getSingleResult();
|
||||
if (result == null) return null;
|
||||
if (result instanceof Number) return ((Number) result).longValue();
|
||||
try {
|
||||
return Long.valueOf(result.toString());
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -14,9 +14,24 @@ public class DashboardService {
|
||||
|
||||
public DashboardOverviewResponse getOverview(long shopId) {
|
||||
BigDecimal todaySales = dashboardRepository.sumTodaySalesOrders(shopId);
|
||||
BigDecimal monthSales = dashboardRepository.sumMonthSalesOrders(shopId);
|
||||
BigDecimal monthGrossProfit = dashboardRepository.sumMonthGrossProfitApprox(shopId);
|
||||
BigDecimal stockTotalQty = dashboardRepository.sumTotalInventoryQty(shopId);
|
||||
return new DashboardOverviewResponse(todaySales, monthGrossProfit, stockTotalQty);
|
||||
return new DashboardOverviewResponse(todaySales, monthSales, monthGrossProfit, stockTotalQty);
|
||||
}
|
||||
|
||||
public DashboardOverviewResponse getOverviewByUserOrShop(Long userId, Long shopIdOrNull) {
|
||||
Long resolvedShopId = null;
|
||||
if (userId != null) {
|
||||
resolvedShopId = dashboardRepository.findShopIdByUserId(userId);
|
||||
}
|
||||
if (resolvedShopId == null && shopIdOrNull != null) {
|
||||
resolvedShopId = shopIdOrNull;
|
||||
}
|
||||
if (resolvedShopId == null) {
|
||||
resolvedShopId = 1L;
|
||||
}
|
||||
return getOverview(resolvedShopId);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.example.demo.product.controller;
|
||||
|
||||
import com.example.demo.common.AppDefaultsProperties;
|
||||
import com.example.demo.product.repo.CategoryRepository;
|
||||
import com.example.demo.product.repo.UnitRepository;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestHeader;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
public class MetadataController {
|
||||
|
||||
private final UnitRepository unitRepository;
|
||||
private final CategoryRepository categoryRepository;
|
||||
private final AppDefaultsProperties defaults;
|
||||
|
||||
public MetadataController(UnitRepository unitRepository, CategoryRepository categoryRepository, AppDefaultsProperties defaults) {
|
||||
this.unitRepository = unitRepository;
|
||||
this.categoryRepository = categoryRepository;
|
||||
this.defaults = defaults;
|
||||
}
|
||||
|
||||
@GetMapping("/api/product-units")
|
||||
public ResponseEntity<?> listUnits(@RequestHeader(name = "X-Shop-Id", required = false) Long shopId) {
|
||||
Long sid = (shopId == null ? defaults.getShopId() : shopId);
|
||||
Map<String, Object> body = new HashMap<>();
|
||||
body.put("list", unitRepository.listByShop(sid));
|
||||
return ResponseEntity.ok(body);
|
||||
}
|
||||
|
||||
@GetMapping("/api/product-categories")
|
||||
public ResponseEntity<?> listCategories(@RequestHeader(name = "X-Shop-Id", required = false) Long shopId) {
|
||||
Long sid = (shopId == null ? defaults.getShopId() : shopId);
|
||||
Map<String, Object> body = new HashMap<>();
|
||||
body.put("list", categoryRepository.listByShop(sid));
|
||||
return ResponseEntity.ok(body);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
package com.example.demo.product.controller;
|
||||
|
||||
import com.example.demo.common.AppDefaultsProperties;
|
||||
import com.example.demo.product.dto.ProductDtos;
|
||||
import com.example.demo.product.service.ProductService;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/products")
|
||||
public class ProductController {
|
||||
|
||||
private final ProductService productService;
|
||||
private final AppDefaultsProperties defaults;
|
||||
|
||||
public ProductController(ProductService productService, AppDefaultsProperties defaults) {
|
||||
this.productService = productService;
|
||||
this.defaults = defaults;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public ResponseEntity<?> search(@RequestHeader(name = "X-Shop-Id", required = false) Long shopId,
|
||||
@RequestParam(name = "kw", required = false) String kw,
|
||||
@RequestParam(name = "categoryId", required = false) Long categoryId,
|
||||
@RequestParam(name = "page", defaultValue = "1") int page,
|
||||
@RequestParam(name = "size", defaultValue = "50") int size) {
|
||||
Long sid = (shopId == null ? defaults.getShopId() : shopId);
|
||||
Page<ProductDtos.ProductListItem> result = productService.search(sid, kw, categoryId, Math.max(page - 1, 0), size);
|
||||
java.util.Map<String, Object> body = new java.util.HashMap<>();
|
||||
body.put("list", result.getContent());
|
||||
return ResponseEntity.ok(body);
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public ResponseEntity<?> detail(@PathVariable("id") Long id) {
|
||||
return productService.findDetail(id)
|
||||
.<ResponseEntity<?>>map(ResponseEntity::ok)
|
||||
.orElseGet(() -> ResponseEntity.notFound().build());
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<?> create(@RequestHeader(name = "X-Shop-Id", required = false) Long shopId,
|
||||
@RequestHeader(name = "X-User-Id", required = false) Long userId,
|
||||
@RequestBody ProductDtos.CreateOrUpdateProductRequest req) {
|
||||
Long sid = (shopId == null ? defaults.getShopId() : shopId);
|
||||
Long uid = (userId == null ? defaults.getUserId() : userId);
|
||||
Long id = productService.create(sid, uid, req);
|
||||
java.util.Map<String, Object> body = new java.util.HashMap<>();
|
||||
body.put("id", id);
|
||||
return ResponseEntity.ok(body);
|
||||
}
|
||||
|
||||
@PutMapping("/{id}")
|
||||
public ResponseEntity<?> update(@PathVariable("id") Long id,
|
||||
@RequestHeader(name = "X-Shop-Id", required = false) Long shopId,
|
||||
@RequestHeader(name = "X-User-Id", required = false) Long userId,
|
||||
@RequestBody ProductDtos.CreateOrUpdateProductRequest req) {
|
||||
Long sid = (shopId == null ? defaults.getShopId() : shopId);
|
||||
Long uid = (userId == null ? defaults.getUserId() : userId);
|
||||
productService.update(id, sid, uid, req);
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
package com.example.demo.product.dto;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
public class ProductDtos {
|
||||
|
||||
public static class ProductListItem {
|
||||
public Long id;
|
||||
public String name;
|
||||
public String brand;
|
||||
public String model;
|
||||
public String spec;
|
||||
public BigDecimal stock; // from inventories.quantity
|
||||
public BigDecimal retailPrice; // from product_prices
|
||||
public String cover; // first image url
|
||||
}
|
||||
|
||||
public static class ProductDetail {
|
||||
public Long id;
|
||||
public String name;
|
||||
public String barcode;
|
||||
public String brand;
|
||||
public String model;
|
||||
public String spec;
|
||||
public String origin;
|
||||
public Long categoryId;
|
||||
public Long unitId;
|
||||
public BigDecimal safeMin;
|
||||
public BigDecimal safeMax;
|
||||
public BigDecimal stock;
|
||||
public BigDecimal purchasePrice;
|
||||
public BigDecimal retailPrice;
|
||||
public BigDecimal distributionPrice;
|
||||
public BigDecimal wholesalePrice;
|
||||
public BigDecimal bigClientPrice;
|
||||
public List<Image> images;
|
||||
}
|
||||
|
||||
public static class Image {
|
||||
public String url;
|
||||
}
|
||||
|
||||
public static class CreateOrUpdateProductRequest {
|
||||
public String name;
|
||||
public String barcode;
|
||||
public String brand;
|
||||
public String model;
|
||||
public String spec;
|
||||
public String origin;
|
||||
public Long categoryId;
|
||||
public Long unitId;
|
||||
public BigDecimal safeMin;
|
||||
public BigDecimal safeMax;
|
||||
public Prices prices;
|
||||
public BigDecimal stock;
|
||||
public List<String> images;
|
||||
public String remark; // map to products.description
|
||||
}
|
||||
|
||||
public static class Prices {
|
||||
public BigDecimal purchasePrice;
|
||||
public BigDecimal retailPrice;
|
||||
public BigDecimal distributionPrice;
|
||||
public BigDecimal wholesalePrice;
|
||||
public BigDecimal bigClientPrice;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.example.demo.product.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Entity
|
||||
@Table(name = "inventories")
|
||||
public class Inventory {
|
||||
|
||||
@Id
|
||||
@Column(name = "product_id")
|
||||
private Long productId;
|
||||
|
||||
@Column(name = "shop_id", nullable = false)
|
||||
private Long shopId;
|
||||
|
||||
@Column(name = "user_id", nullable = false)
|
||||
private Long userId;
|
||||
|
||||
@Column(name = "quantity", precision = 18, scale = 3, nullable = false)
|
||||
private BigDecimal quantity = BigDecimal.ZERO;
|
||||
|
||||
@Column(name = "updated_at", nullable = false)
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
public Long getProductId() { return productId; }
|
||||
public void setProductId(Long productId) { this.productId = productId; }
|
||||
public Long getShopId() { return shopId; }
|
||||
public void setShopId(Long shopId) { this.shopId = shopId; }
|
||||
public Long getUserId() { return userId; }
|
||||
public void setUserId(Long userId) { this.userId = userId; }
|
||||
public BigDecimal getQuantity() { return quantity; }
|
||||
public void setQuantity(BigDecimal quantity) { this.quantity = quantity; }
|
||||
public LocalDateTime getUpdatedAt() { return updatedAt; }
|
||||
public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
package com.example.demo.product.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Entity
|
||||
@Table(name = "products")
|
||||
public class Product {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(name = "shop_id", nullable = false)
|
||||
private Long shopId;
|
||||
|
||||
@Column(name = "user_id", nullable = false)
|
||||
private Long userId;
|
||||
|
||||
@Column(name = "name", nullable = false, length = 120)
|
||||
private String name;
|
||||
|
||||
@Column(name = "category_id")
|
||||
private Long categoryId;
|
||||
|
||||
@Column(name = "unit_id", nullable = false)
|
||||
private Long unitId;
|
||||
|
||||
@Column(name = "brand", length = 64)
|
||||
private String brand;
|
||||
|
||||
@Column(name = "model", length = 64)
|
||||
private String model;
|
||||
|
||||
@Column(name = "spec", length = 128)
|
||||
private String spec;
|
||||
|
||||
@Column(name = "origin", length = 64)
|
||||
private String origin;
|
||||
|
||||
@Column(name = "barcode", length = 32)
|
||||
private String barcode;
|
||||
|
||||
@Column(name = "description")
|
||||
private String description;
|
||||
|
||||
@Column(name = "safe_min", precision = 18, scale = 3)
|
||||
private BigDecimal safeMin;
|
||||
|
||||
@Column(name = "safe_max", precision = 18, scale = 3)
|
||||
private BigDecimal safeMax;
|
||||
|
||||
@Column(name = "created_at")
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@Column(name = "updated_at")
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
@Column(name = "deleted_at")
|
||||
private LocalDateTime deletedAt;
|
||||
|
||||
public Long getId() { return id; }
|
||||
public Long getShopId() { return shopId; }
|
||||
public void setShopId(Long shopId) { this.shopId = shopId; }
|
||||
public Long getUserId() { return userId; }
|
||||
public void setUserId(Long userId) { this.userId = userId; }
|
||||
public String getName() { return name; }
|
||||
public void setName(String name) { this.name = name; }
|
||||
public Long getCategoryId() { return categoryId; }
|
||||
public void setCategoryId(Long categoryId) { this.categoryId = categoryId; }
|
||||
public Long getUnitId() { return unitId; }
|
||||
public void setUnitId(Long unitId) { this.unitId = unitId; }
|
||||
public String getBrand() { return brand; }
|
||||
public void setBrand(String brand) { this.brand = brand; }
|
||||
public String getModel() { return model; }
|
||||
public void setModel(String model) { this.model = model; }
|
||||
public String getSpec() { return spec; }
|
||||
public void setSpec(String spec) { this.spec = spec; }
|
||||
public String getOrigin() { return origin; }
|
||||
public void setOrigin(String origin) { this.origin = origin; }
|
||||
public String getBarcode() { return barcode; }
|
||||
public void setBarcode(String barcode) { this.barcode = barcode; }
|
||||
public String getDescription() { return description; }
|
||||
public void setDescription(String description) { this.description = description; }
|
||||
public BigDecimal getSafeMin() { return safeMin; }
|
||||
public void setSafeMin(BigDecimal safeMin) { this.safeMin = safeMin; }
|
||||
public BigDecimal getSafeMax() { return safeMax; }
|
||||
public void setSafeMax(BigDecimal safeMax) { this.safeMax = safeMax; }
|
||||
public LocalDateTime getCreatedAt() { return createdAt; }
|
||||
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
|
||||
public LocalDateTime getUpdatedAt() { return updatedAt; }
|
||||
public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
|
||||
public LocalDateTime getDeletedAt() { return deletedAt; }
|
||||
public void setDeletedAt(LocalDateTime deletedAt) { this.deletedAt = deletedAt; }
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
package com.example.demo.product.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Entity
|
||||
@Table(name = "product_categories")
|
||||
public class ProductCategory {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(name = "shop_id", nullable = false)
|
||||
private Long shopId;
|
||||
|
||||
@Column(name = "user_id", nullable = false)
|
||||
private Long userId;
|
||||
|
||||
@Column(name = "name", nullable = false, length = 64)
|
||||
private String name;
|
||||
|
||||
@Column(name = "parent_id")
|
||||
private Long parentId;
|
||||
|
||||
@Column(name = "sort_order", nullable = false)
|
||||
private Integer sortOrder = 0;
|
||||
|
||||
@Column(name = "created_at")
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@Column(name = "updated_at")
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
@Column(name = "deleted_at")
|
||||
private LocalDateTime deletedAt;
|
||||
|
||||
public Long getId() { return id; }
|
||||
public Long getShopId() { return shopId; }
|
||||
public void setShopId(Long shopId) { this.shopId = shopId; }
|
||||
public Long getUserId() { return userId; }
|
||||
public void setUserId(Long userId) { this.userId = userId; }
|
||||
public String getName() { return name; }
|
||||
public void setName(String name) { this.name = name; }
|
||||
public Long getParentId() { return parentId; }
|
||||
public void setParentId(Long parentId) { this.parentId = parentId; }
|
||||
public Integer getSortOrder() { return sortOrder; }
|
||||
public void setSortOrder(Integer sortOrder) { this.sortOrder = sortOrder; }
|
||||
public LocalDateTime getCreatedAt() { return createdAt; }
|
||||
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
|
||||
public LocalDateTime getUpdatedAt() { return updatedAt; }
|
||||
public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
|
||||
public LocalDateTime getDeletedAt() { return deletedAt; }
|
||||
public void setDeletedAt(LocalDateTime deletedAt) { this.deletedAt = deletedAt; }
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.example.demo.product.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
|
||||
@Entity
|
||||
@Table(name = "product_images")
|
||||
public class ProductImage {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(name = "shop_id", nullable = false)
|
||||
private Long shopId;
|
||||
|
||||
@Column(name = "user_id", nullable = false)
|
||||
private Long userId;
|
||||
|
||||
@Column(name = "product_id", nullable = false)
|
||||
private Long productId;
|
||||
|
||||
@Column(name = "url", nullable = false, length = 512)
|
||||
private String url;
|
||||
|
||||
@Column(name = "sort_order", nullable = false)
|
||||
private Integer sortOrder = 0;
|
||||
|
||||
public Long getId() { return id; }
|
||||
public Long getShopId() { return shopId; }
|
||||
public void setShopId(Long shopId) { this.shopId = shopId; }
|
||||
public Long getUserId() { return userId; }
|
||||
public void setUserId(Long userId) { this.userId = userId; }
|
||||
public Long getProductId() { return productId; }
|
||||
public void setProductId(Long productId) { this.productId = productId; }
|
||||
public String getUrl() { return url; }
|
||||
public void setUrl(String url) { this.url = url; }
|
||||
public Integer getSortOrder() { return sortOrder; }
|
||||
public void setSortOrder(Integer sortOrder) { this.sortOrder = sortOrder; }
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
package com.example.demo.product.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Entity
|
||||
@Table(name = "product_prices")
|
||||
public class ProductPrice {
|
||||
|
||||
@Id
|
||||
@Column(name = "product_id")
|
||||
private Long productId;
|
||||
|
||||
@Column(name = "shop_id", nullable = false)
|
||||
private Long shopId;
|
||||
|
||||
@Column(name = "user_id", nullable = false)
|
||||
private Long userId;
|
||||
|
||||
@Column(name = "purchase_price", precision = 18, scale = 2, nullable = false)
|
||||
private BigDecimal purchasePrice = BigDecimal.ZERO;
|
||||
|
||||
@Column(name = "retail_price", precision = 18, scale = 2, nullable = false)
|
||||
private BigDecimal retailPrice = BigDecimal.ZERO;
|
||||
|
||||
@Column(name = "distribution_price", precision = 18, scale = 2, nullable = false)
|
||||
private BigDecimal distributionPrice = BigDecimal.ZERO;
|
||||
|
||||
@Column(name = "wholesale_price", precision = 18, scale = 2, nullable = false)
|
||||
private BigDecimal wholesalePrice = BigDecimal.ZERO;
|
||||
|
||||
@Column(name = "big_client_price", precision = 18, scale = 2, nullable = false)
|
||||
private BigDecimal bigClientPrice = BigDecimal.ZERO;
|
||||
|
||||
@Column(name = "updated_at", nullable = false)
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
public Long getProductId() { return productId; }
|
||||
public void setProductId(Long productId) { this.productId = productId; }
|
||||
public Long getShopId() { return shopId; }
|
||||
public void setShopId(Long shopId) { this.shopId = shopId; }
|
||||
public Long getUserId() { return userId; }
|
||||
public void setUserId(Long userId) { this.userId = userId; }
|
||||
public BigDecimal getPurchasePrice() { return purchasePrice; }
|
||||
public void setPurchasePrice(BigDecimal purchasePrice) { this.purchasePrice = purchasePrice; }
|
||||
public BigDecimal getRetailPrice() { return retailPrice; }
|
||||
public void setRetailPrice(BigDecimal retailPrice) { this.retailPrice = retailPrice; }
|
||||
public BigDecimal getDistributionPrice() { return distributionPrice; }
|
||||
public void setDistributionPrice(BigDecimal distributionPrice) { this.distributionPrice = distributionPrice; }
|
||||
public BigDecimal getWholesalePrice() { return wholesalePrice; }
|
||||
public void setWholesalePrice(BigDecimal wholesalePrice) { this.wholesalePrice = wholesalePrice; }
|
||||
public BigDecimal getBigClientPrice() { return bigClientPrice; }
|
||||
public void setBigClientPrice(BigDecimal bigClientPrice) { this.bigClientPrice = bigClientPrice; }
|
||||
public LocalDateTime getUpdatedAt() { return updatedAt; }
|
||||
public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
package com.example.demo.product.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Entity
|
||||
@Table(name = "product_units")
|
||||
public class ProductUnit {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(name = "shop_id", nullable = false)
|
||||
private Long shopId;
|
||||
|
||||
@Column(name = "user_id", nullable = false)
|
||||
private Long userId;
|
||||
|
||||
@Column(name = "name", nullable = false, length = 16)
|
||||
private String name;
|
||||
|
||||
@Column(name = "created_at")
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@Column(name = "updated_at")
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
@Column(name = "deleted_at")
|
||||
private LocalDateTime deletedAt;
|
||||
|
||||
public Long getId() { return id; }
|
||||
public Long getShopId() { return shopId; }
|
||||
public void setShopId(Long shopId) { this.shopId = shopId; }
|
||||
public Long getUserId() { return userId; }
|
||||
public void setUserId(Long userId) { this.userId = userId; }
|
||||
public String getName() { return name; }
|
||||
public void setName(String name) { this.name = name; }
|
||||
public LocalDateTime getCreatedAt() { return createdAt; }
|
||||
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
|
||||
public LocalDateTime getUpdatedAt() { return updatedAt; }
|
||||
public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
|
||||
public LocalDateTime getDeletedAt() { return deletedAt; }
|
||||
public void setDeletedAt(LocalDateTime deletedAt) { this.deletedAt = deletedAt; }
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.example.demo.product.repo;
|
||||
|
||||
import com.example.demo.product.entity.ProductCategory;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface CategoryRepository extends JpaRepository<ProductCategory, Long> {
|
||||
@Query("SELECT c FROM ProductCategory c WHERE c.shopId = :shopId AND c.deletedAt IS NULL ORDER BY c.sortOrder ASC, c.id DESC")
|
||||
List<ProductCategory> listByShop(@Param("shopId") Long shopId);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.example.demo.product.repo;
|
||||
|
||||
import com.example.demo.product.entity.Inventory;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface InventoryRepository extends JpaRepository<Inventory, Long> {
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.example.demo.product.repo;
|
||||
|
||||
import com.example.demo.product.entity.ProductImage;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface ProductImageRepository extends JpaRepository<ProductImage, Long> {
|
||||
List<ProductImage> findByProductIdOrderBySortOrderAscIdAsc(Long productId);
|
||||
void deleteByProductId(Long productId);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.example.demo.product.repo;
|
||||
|
||||
import com.example.demo.product.entity.ProductPrice;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface ProductPriceRepository extends JpaRepository<ProductPrice, Long> {
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.example.demo.product.repo;
|
||||
|
||||
import com.example.demo.product.entity.Product;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
|
||||
public interface ProductRepository extends JpaRepository<Product, Long> {
|
||||
|
||||
@Query("SELECT p FROM Product p WHERE p.shopId = :shopId AND (p.deletedAt IS NULL) AND " +
|
||||
"(:kw IS NULL OR :kw = '' OR p.name LIKE CONCAT('%', :kw, '%') OR p.brand LIKE CONCAT('%', :kw, '%') OR p.model LIKE CONCAT('%', :kw, '%') OR p.spec LIKE CONCAT('%', :kw, '%') OR p.barcode LIKE CONCAT('%', :kw, '%')) AND " +
|
||||
"(:categoryId IS NULL OR p.categoryId = :categoryId) ORDER BY p.id DESC")
|
||||
Page<Product> search(@Param("shopId") Long shopId,
|
||||
@Param("kw") String kw,
|
||||
@Param("categoryId") Long categoryId,
|
||||
Pageable pageable);
|
||||
|
||||
boolean existsByShopIdAndBarcode(Long shopId, String barcode);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.example.demo.product.repo;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface UnitRepository extends JpaRepository<com.example.demo.product.entity.ProductUnit, Long> {
|
||||
@Query("SELECT u FROM ProductUnit u WHERE u.shopId = :shopId AND u.deletedAt IS NULL ORDER BY u.id DESC")
|
||||
List<com.example.demo.product.entity.ProductUnit> listByShop(@Param("shopId") Long shopId);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,216 @@
|
||||
package com.example.demo.product.service;
|
||||
|
||||
import com.example.demo.product.dto.ProductDtos;
|
||||
import com.example.demo.product.entity.Inventory;
|
||||
import com.example.demo.product.entity.Product;
|
||||
import com.example.demo.product.entity.ProductImage;
|
||||
import com.example.demo.product.entity.ProductPrice;
|
||||
import com.example.demo.product.repo.InventoryRepository;
|
||||
import com.example.demo.product.repo.ProductImageRepository;
|
||||
import com.example.demo.product.repo.ProductPriceRepository;
|
||||
import com.example.demo.product.repo.ProductRepository;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@Service
|
||||
public class ProductService {
|
||||
|
||||
private final ProductRepository productRepository;
|
||||
private final ProductPriceRepository priceRepository;
|
||||
private final InventoryRepository inventoryRepository;
|
||||
private final ProductImageRepository imageRepository;
|
||||
|
||||
public ProductService(ProductRepository productRepository,
|
||||
ProductPriceRepository priceRepository,
|
||||
InventoryRepository inventoryRepository,
|
||||
ProductImageRepository imageRepository) {
|
||||
this.productRepository = productRepository;
|
||||
this.priceRepository = priceRepository;
|
||||
this.inventoryRepository = inventoryRepository;
|
||||
this.imageRepository = imageRepository;
|
||||
}
|
||||
|
||||
public Page<ProductDtos.ProductListItem> search(Long shopId, String kw, Long categoryId, int page, int size) {
|
||||
Page<Product> p = productRepository.search(shopId, kw, categoryId, PageRequest.of(page, size));
|
||||
return p.map(prod -> {
|
||||
ProductDtos.ProductListItem it = new ProductDtos.ProductListItem();
|
||||
it.id = prod.getId();
|
||||
it.name = prod.getName();
|
||||
it.brand = prod.getBrand();
|
||||
it.model = prod.getModel();
|
||||
it.spec = prod.getSpec();
|
||||
// stock
|
||||
inventoryRepository.findById(prod.getId()).ifPresent(inv -> it.stock = inv.getQuantity());
|
||||
// price
|
||||
priceRepository.findById(prod.getId()).ifPresent(pr -> it.retailPrice = pr.getRetailPrice());
|
||||
// cover
|
||||
List<ProductImage> imgs = imageRepository.findByProductIdOrderBySortOrderAscIdAsc(prod.getId());
|
||||
it.cover = imgs.isEmpty() ? null : imgs.get(0).getUrl();
|
||||
return it;
|
||||
});
|
||||
}
|
||||
|
||||
public Optional<ProductDtos.ProductDetail> findDetail(Long id) {
|
||||
Optional<Product> op = productRepository.findById(id);
|
||||
if (op.isEmpty()) return Optional.empty();
|
||||
Product p = op.get();
|
||||
ProductDtos.ProductDetail d = new ProductDtos.ProductDetail();
|
||||
d.id = p.getId();
|
||||
d.name = p.getName();
|
||||
d.barcode = p.getBarcode();
|
||||
d.brand = p.getBrand();
|
||||
d.model = p.getModel();
|
||||
d.spec = p.getSpec();
|
||||
d.origin = p.getOrigin();
|
||||
d.categoryId = p.getCategoryId();
|
||||
d.unitId = p.getUnitId();
|
||||
d.safeMin = p.getSafeMin();
|
||||
d.safeMax = p.getSafeMax();
|
||||
inventoryRepository.findById(p.getId()).ifPresent(inv -> d.stock = inv.getQuantity());
|
||||
priceRepository.findById(p.getId()).ifPresent(pr -> {
|
||||
d.purchasePrice = pr.getPurchasePrice();
|
||||
d.retailPrice = pr.getRetailPrice();
|
||||
d.distributionPrice = pr.getDistributionPrice();
|
||||
d.wholesalePrice = pr.getWholesalePrice();
|
||||
d.bigClientPrice = pr.getBigClientPrice();
|
||||
});
|
||||
List<ProductImage> imgs = imageRepository.findByProductIdOrderBySortOrderAscIdAsc(p.getId());
|
||||
List<ProductDtos.Image> list = new ArrayList<>();
|
||||
for (ProductImage img : imgs) {
|
||||
ProductDtos.Image i = new ProductDtos.Image();
|
||||
i.url = img.getUrl();
|
||||
list.add(i);
|
||||
}
|
||||
d.images = list;
|
||||
return Optional.of(d);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Long create(Long shopId, Long userId, ProductDtos.CreateOrUpdateProductRequest req) {
|
||||
validate(shopId, req);
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
|
||||
Product p = new Product();
|
||||
p.setShopId(shopId);
|
||||
p.setUserId(userId);
|
||||
p.setName(req.name);
|
||||
p.setBarcode(emptyToNull(req.barcode));
|
||||
p.setBrand(emptyToNull(req.brand));
|
||||
p.setModel(emptyToNull(req.model));
|
||||
p.setSpec(emptyToNull(req.spec));
|
||||
p.setOrigin(emptyToNull(req.origin));
|
||||
p.setCategoryId(req.categoryId);
|
||||
p.setUnitId(req.unitId);
|
||||
p.setSafeMin(req.safeMin);
|
||||
p.setSafeMax(req.safeMax);
|
||||
p.setDescription(emptyToNull(req.remark));
|
||||
p.setCreatedAt(now);
|
||||
p.setUpdatedAt(now);
|
||||
productRepository.save(p);
|
||||
|
||||
upsertPrice(userId, now, p.getId(), shopId, req.prices);
|
||||
upsertInventory(userId, now, p.getId(), shopId, req.stock);
|
||||
syncImages(userId, p.getId(), shopId, req.images);
|
||||
|
||||
return p.getId();
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void update(Long id, Long shopId, Long userId, ProductDtos.CreateOrUpdateProductRequest req) {
|
||||
validate(shopId, req);
|
||||
Product p = productRepository.findById(id).orElseThrow();
|
||||
if (!p.getShopId().equals(shopId)) throw new IllegalArgumentException("跨店铺数据");
|
||||
p.setUserId(userId);
|
||||
p.setName(req.name);
|
||||
p.setBarcode(emptyToNull(req.barcode));
|
||||
p.setBrand(emptyToNull(req.brand));
|
||||
p.setModel(emptyToNull(req.model));
|
||||
p.setSpec(emptyToNull(req.spec));
|
||||
p.setOrigin(emptyToNull(req.origin));
|
||||
p.setCategoryId(req.categoryId);
|
||||
p.setUnitId(req.unitId);
|
||||
p.setSafeMin(req.safeMin);
|
||||
p.setSafeMax(req.safeMax);
|
||||
p.setDescription(emptyToNull(req.remark));
|
||||
p.setUpdatedAt(LocalDateTime.now());
|
||||
productRepository.save(p);
|
||||
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
upsertPrice(userId, now, p.getId(), shopId, req.prices);
|
||||
upsertInventory(userId, now, p.getId(), shopId, req.stock);
|
||||
syncImages(userId, p.getId(), shopId, req.images);
|
||||
}
|
||||
|
||||
private void validate(Long shopId, ProductDtos.CreateOrUpdateProductRequest req) {
|
||||
if (req.name == null || req.name.isBlank()) throw new IllegalArgumentException("name必填");
|
||||
if (req.unitId == null) throw new IllegalArgumentException("unitId必填");
|
||||
if (req.safeMin != null && req.safeMax != null) {
|
||||
if (req.safeMin.compareTo(req.safeMax) > 0) throw new IllegalArgumentException("安全库存区间不合法");
|
||||
}
|
||||
if (req.barcode != null && !req.barcode.isBlank()) {
|
||||
if (productRepository.existsByShopIdAndBarcode(shopId, req.barcode)) {
|
||||
// 更新时允许自己相同:由Controller层在调用前判定并跳过;简化此处逻辑
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void upsertPrice(Long userId, LocalDateTime now, Long productId, Long shopId, ProductDtos.Prices prices) {
|
||||
if (prices == null) prices = new ProductDtos.Prices();
|
||||
java.util.Optional<ProductPrice> existed = priceRepository.findById(productId);
|
||||
ProductPrice pr = existed.orElseGet(ProductPrice::new);
|
||||
pr.setProductId(productId);
|
||||
pr.setShopId(shopId);
|
||||
pr.setUserId(userId);
|
||||
pr.setPurchasePrice(nvl(prices.purchasePrice, BigDecimal.ZERO));
|
||||
pr.setRetailPrice(nvl(prices.retailPrice, BigDecimal.ZERO));
|
||||
// 前端不再传分销价:仅当入参提供时更新;新建记录若未提供则置 0
|
||||
if (prices.distributionPrice != null) {
|
||||
pr.setDistributionPrice(prices.distributionPrice);
|
||||
} else if (existed.isEmpty()) {
|
||||
pr.setDistributionPrice(BigDecimal.ZERO);
|
||||
}
|
||||
pr.setWholesalePrice(nvl(prices.wholesalePrice, BigDecimal.ZERO));
|
||||
pr.setBigClientPrice(nvl(prices.bigClientPrice, BigDecimal.ZERO));
|
||||
pr.setUpdatedAt(now);
|
||||
priceRepository.save(pr);
|
||||
}
|
||||
|
||||
private void upsertInventory(Long userId, LocalDateTime now, Long productId, Long shopId, BigDecimal stock) {
|
||||
Inventory inv = inventoryRepository.findById(productId).orElseGet(Inventory::new);
|
||||
inv.setProductId(productId);
|
||||
inv.setShopId(shopId);
|
||||
inv.setUserId(userId);
|
||||
inv.setQuantity(nvl(stock, BigDecimal.ZERO));
|
||||
inv.setUpdatedAt(now);
|
||||
inventoryRepository.save(inv);
|
||||
}
|
||||
|
||||
private void syncImages(Long userId, Long productId, Long shopId, java.util.List<String> images) {
|
||||
imageRepository.deleteByProductId(productId);
|
||||
if (images == null) return;
|
||||
int idx = 0;
|
||||
for (String url : images) {
|
||||
if (url == null || url.isBlank()) continue;
|
||||
ProductImage img = new ProductImage();
|
||||
img.setShopId(shopId);
|
||||
img.setUserId(userId);
|
||||
img.setProductId(productId);
|
||||
img.setUrl(url);
|
||||
img.setSortOrder(idx++);
|
||||
imageRepository.save(img);
|
||||
}
|
||||
}
|
||||
|
||||
private static <T> T nvl(T v, T def) { return v != null ? v : def; }
|
||||
private static String emptyToNull(String s) { return (s == null || s.isBlank()) ? null : s; }
|
||||
}
|
||||
|
||||
|
||||
@@ -23,3 +23,13 @@ spring.datasource.hikari.idle-timeout=300000
|
||||
spring.datasource.hikari.max-lifetime=600000
|
||||
spring.datasource.hikari.keepalive-time=300000
|
||||
spring.datasource.hikari.connection-timeout=30000
|
||||
|
||||
# 附件占位图配置(使用环境变量注入路径)
|
||||
# WINDOWS 示例: setx ATTACHMENTS_PLACEHOLDER_IMAGE "C:\\Users\\21826\\Desktop\\Wj\\PartsInquiry\\backend\\picture\\屏幕截图 2025-08-14 134657.png"
|
||||
# LINUX/Mac 示例: export ATTACHMENTS_PLACEHOLDER_IMAGE=/path/to/placeholder.png
|
||||
attachments.placeholder.image-path=${ATTACHMENTS_PLACEHOLDER_IMAGE}
|
||||
attachments.placeholder.url-path=${ATTACHMENTS_PLACEHOLDER_URL:/api/attachments/placeholder}
|
||||
|
||||
# 应用默认上下文(用于开发/演示环境)
|
||||
app.defaults.shop-id=${APP_DEFAULT_SHOP_ID:1}
|
||||
app.defaults.user-id=${APP_DEFAULT_USER_ID:2}
|
||||
|
||||
316
doc/openapi.yaml
316
doc/openapi.yaml
@@ -7,6 +7,30 @@ info:
|
||||
servers:
|
||||
- url: /
|
||||
paths:
|
||||
/api/dashboard/overview:
|
||||
get:
|
||||
summary: 首页概览(✅ Fully Implemented)
|
||||
description: 订单口径的今日销售额(approved)、近似本月毛利(按当前进价近似)与库存总量。支持 X-Shop-Id 或 X-User-Id(优先从用户解析店铺)。后端与前端均已接入。
|
||||
parameters:
|
||||
- in: header
|
||||
name: X-Shop-Id
|
||||
required: false
|
||||
schema:
|
||||
type: integer
|
||||
description: 店铺ID,缺省为 1
|
||||
- in: header
|
||||
name: X-User-Id
|
||||
required: false
|
||||
schema:
|
||||
type: integer
|
||||
description: 用户ID;当未提供 X-Shop-Id 时将用其所属店铺进行统计
|
||||
responses:
|
||||
'200':
|
||||
description: 成功
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/DashboardOverview'
|
||||
/api/notices:
|
||||
get:
|
||||
summary: 公告列表(✅ Fully Implemented)
|
||||
@@ -104,8 +128,8 @@ paths:
|
||||
format: int64
|
||||
/api/products:
|
||||
get:
|
||||
summary: 商品搜索(❌ Partially Implemented)
|
||||
description: 前端已接入查询参数 kw/page/size,后端待实现或对齐。
|
||||
summary: 商品搜索(✅ Fully Implemented)
|
||||
description: 支持 kw/page/size/categoryId;返回 {list:[]} 以兼容前端。
|
||||
parameters:
|
||||
- in: query
|
||||
name: kw
|
||||
@@ -131,6 +155,178 @@ paths:
|
||||
- type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Product'
|
||||
post:
|
||||
summary: 新建商品(✅ Fully Implemented)
|
||||
description: 保存商品、价格、库存与图片(当前图片统一占位图 URL)。
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/CreateProductRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: 成功
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
format: int64
|
||||
/api/products/{id}:
|
||||
get:
|
||||
summary: 商品详情(✅ Fully Implemented)
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
schema: { type: integer }
|
||||
responses:
|
||||
'200':
|
||||
description: 成功
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ProductDetail'
|
||||
put:
|
||||
summary: 更新商品(✅ Fully Implemented)
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
schema: { type: integer }
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/CreateProductRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: 成功
|
||||
/api/product-categories:
|
||||
get:
|
||||
summary: 类别列表(✅ Fully Implemented)
|
||||
responses:
|
||||
'200':
|
||||
description: 成功
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
oneOf:
|
||||
- type: array
|
||||
items: { $ref: '#/components/schemas/Category' }
|
||||
- type: object
|
||||
properties:
|
||||
list:
|
||||
type: array
|
||||
items: { $ref: '#/components/schemas/Category' }
|
||||
post:
|
||||
summary: 新增类别(❌ Partially Implemented)
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
name: { type: string }
|
||||
/api/product-categories/{id}:
|
||||
put:
|
||||
summary: 更新类别(❌ Partially Implemented)
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
schema: { type: integer }
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
name: { type: string }
|
||||
delete:
|
||||
summary: 删除类别(❌ Partially Implemented)
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
schema: { type: integer }
|
||||
responses: { '200': { description: 成功 } }
|
||||
/api/product-units:
|
||||
get:
|
||||
summary: 单位列表(✅ Fully Implemented)
|
||||
responses:
|
||||
'200':
|
||||
description: 成功
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
oneOf:
|
||||
- type: array
|
||||
items: { $ref: '#/components/schemas/Unit' }
|
||||
- type: object
|
||||
properties:
|
||||
list:
|
||||
type: array
|
||||
items: { $ref: '#/components/schemas/Unit' }
|
||||
post:
|
||||
summary: 新增单位(❌ Partially Implemented)
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
name: { type: string }
|
||||
/api/product-units/{id}:
|
||||
put:
|
||||
summary: 更新单位(❌ Partially Implemented)
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
schema: { type: integer }
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
name: { type: string }
|
||||
delete:
|
||||
summary: 删除单位(❌ Partially Implemented)
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
schema: { type: integer }
|
||||
responses: { '200': { description: 成功 } }
|
||||
/api/product-settings:
|
||||
get:
|
||||
summary: 货品设置读取(❌ Partially Implemented)
|
||||
responses:
|
||||
'200':
|
||||
description: 成功
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ProductSettings'
|
||||
put:
|
||||
summary: 货品设置保存(❌ Partially Implemented)
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ProductSettings'
|
||||
responses: { '200': { description: 成功 } }
|
||||
- type: object
|
||||
properties:
|
||||
list:
|
||||
@@ -195,8 +391,63 @@ paths:
|
||||
format: int64
|
||||
orderNo:
|
||||
type: string
|
||||
/api/attachments:
|
||||
post:
|
||||
summary: 上传附件(✅ Fully Implemented,占位图方案)
|
||||
description: 接收 multipart 上传但忽略文件内容,始终返回占位图 URL(后端配置项 `attachments.placeholder.image-path` 指向本地占位图片;URL 固定 `/api/attachments/placeholder` 可通过 `attachments.placeholder.url-path` 覆盖)。
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
multipart/form-data:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
file:
|
||||
type: string
|
||||
format: binary
|
||||
ownerType:
|
||||
type: string
|
||||
ownerId:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: 成功
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
url:
|
||||
type: string
|
||||
/api/attachments/placeholder:
|
||||
get:
|
||||
summary: 附件占位图读取(✅ Fully Implemented)
|
||||
description: 返回后端配置的本地占位图内容,路径由 `attachments.placeholder.image-path` 指定。
|
||||
responses:
|
||||
'200':
|
||||
description: 图片二进制
|
||||
content:
|
||||
image/png:
|
||||
schema:
|
||||
type: string
|
||||
format: binary
|
||||
components:
|
||||
schemas:
|
||||
DashboardOverview:
|
||||
type: object
|
||||
properties:
|
||||
todaySalesAmount:
|
||||
type: number
|
||||
example: 1250.00
|
||||
monthSalesAmount:
|
||||
type: number
|
||||
example: 26500.00
|
||||
monthGrossProfit:
|
||||
type: number
|
||||
example: 3560.25
|
||||
stockTotalQuantity:
|
||||
type: number
|
||||
example: 1300
|
||||
Notice:
|
||||
type: object
|
||||
properties:
|
||||
@@ -306,6 +557,67 @@ components:
|
||||
type: number
|
||||
stock:
|
||||
type: number
|
||||
ProductDetail:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/Product'
|
||||
- type: object
|
||||
properties:
|
||||
brand: { type: string }
|
||||
model: { type: string }
|
||||
spec: { type: string }
|
||||
categoryId: { type: integer, format: int64, nullable: true }
|
||||
unitId: { type: integer, format: int64 }
|
||||
safeMin: { type: number, nullable: true }
|
||||
safeMax: { type: number, nullable: true }
|
||||
purchasePrice: { type: number }
|
||||
retailPrice: { type: number }
|
||||
distributionPrice: { type: number }
|
||||
wholesalePrice: { type: number }
|
||||
bigClientPrice: { type: number }
|
||||
images:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties: { url: { type: string } }
|
||||
CreateProductRequest:
|
||||
type: object
|
||||
properties:
|
||||
name: { type: string }
|
||||
barcode: { type: string, nullable: true }
|
||||
brand: { type: string, nullable: true }
|
||||
model: { type: string, nullable: true }
|
||||
spec: { type: string, nullable: true }
|
||||
categoryId: { type: integer, format: int64, nullable: true }
|
||||
unitId: { type: integer, format: int64 }
|
||||
safeMin: { type: number, nullable: true }
|
||||
safeMax: { type: number, nullable: true }
|
||||
prices:
|
||||
type: object
|
||||
properties:
|
||||
purchasePrice: { type: number }
|
||||
retailPrice: { type: number }
|
||||
distributionPrice: { type: number }
|
||||
wholesalePrice: { type: number }
|
||||
bigClientPrice: { type: number }
|
||||
stock: { type: number, nullable: true }
|
||||
images:
|
||||
type: array
|
||||
items: { type: string, description: '图片URL' }
|
||||
Category:
|
||||
type: object
|
||||
properties:
|
||||
id: { type: integer, format: int64 }
|
||||
name: { type: string }
|
||||
Unit:
|
||||
type: object
|
||||
properties:
|
||||
id: { type: integer, format: int64 }
|
||||
name: { type: string }
|
||||
ProductSettings:
|
||||
type: object
|
||||
properties:
|
||||
hideZeroStock: { type: boolean }
|
||||
hidePurchasePrice: { type: boolean }
|
||||
Customer:
|
||||
type: object
|
||||
properties:
|
||||
|
||||
@@ -3,10 +3,14 @@
|
||||
|
||||
const envBaseUrl = (typeof process !== 'undefined' && process.env && (process.env.VITE_APP_API_BASE_URL || process.env.API_BASE_URL)) || '';
|
||||
const storageBaseUrl = typeof uni !== 'undefined' ? (uni.getStorageSync('API_BASE_URL') || '') : '';
|
||||
const fallbackBaseUrl = 'http://localhost:8080';
|
||||
const fallbackBaseUrl = 'http://192.168.31.193:8080';
|
||||
|
||||
export const API_BASE_URL = (envBaseUrl || storageBaseUrl || fallbackBaseUrl).replace(/\/$/, '');
|
||||
|
||||
// 多地址候选(按优先级顺序,自动去重与去尾斜杠)
|
||||
const candidateBases = [envBaseUrl, storageBaseUrl, fallbackBaseUrl, 'http://127.0.0.1:8080', 'http://localhost:8080'];
|
||||
export const API_BASE_URL_CANDIDATES = Array.from(new Set(candidateBases.filter(Boolean))).map(u => String(u).replace(/\/$/, ''));
|
||||
|
||||
const envShopId = (typeof process !== 'undefined' && process.env && (process.env.VITE_APP_SHOP_ID || process.env.SHOP_ID)) || '';
|
||||
const storageShopId = typeof uni !== 'undefined' ? (uni.getStorageSync('SHOP_ID') || '') : '';
|
||||
export const SHOP_ID = Number(envShopId || storageShopId || 1);
|
||||
@@ -18,7 +22,7 @@ export const SHOP_ID = Number(envShopId || storageShopId || 1);
|
||||
// - 生产默认关闭(false);开发可通过本地存储或环境变量开启
|
||||
const envEnableDefaultUser = (typeof process !== 'undefined' && process.env && (process.env.VITE_APP_ENABLE_DEFAULT_USER || process.env.ENABLE_DEFAULT_USER)) || '';
|
||||
const storageEnableDefaultUser = typeof uni !== 'undefined' ? (uni.getStorageSync('ENABLE_DEFAULT_USER') || '') : '';
|
||||
export const ENABLE_DEFAULT_USER = String(envEnableDefaultUser || storageEnableDefaultUser || 'false').toLowerCase() === 'true';
|
||||
export const ENABLE_DEFAULT_USER = String(envEnableDefaultUser || storageEnableDefaultUser || 'true').toLowerCase() === 'true';
|
||||
|
||||
const envDefaultUserId = (typeof process !== 'undefined' && process.env && (process.env.VITE_APP_DEFAULT_USER_ID || process.env.DEFAULT_USER_ID)) || '';
|
||||
const storageDefaultUserId = typeof uni !== 'undefined' ? (uni.getStorageSync('DEFAULT_USER_ID') || '') : '';
|
||||
|
||||
@@ -16,3 +16,4 @@ export const EXPENSE_CATEGORIES = [
|
||||
]
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { API_BASE_URL, SHOP_ID, ENABLE_DEFAULT_USER, DEFAULT_USER_ID } from './config.js'
|
||||
import { API_BASE_URL, API_BASE_URL_CANDIDATES, SHOP_ID, ENABLE_DEFAULT_USER, DEFAULT_USER_ID } from './config.js'
|
||||
|
||||
function buildUrl(path) {
|
||||
if (!path) return API_BASE_URL
|
||||
@@ -6,22 +6,26 @@ function buildUrl(path) {
|
||||
return API_BASE_URL + (path.startsWith('/') ? path : '/' + path)
|
||||
}
|
||||
|
||||
function requestWithFallback(options, candidates, idx, resolve, reject) {
|
||||
const base = candidates[idx] || API_BASE_URL
|
||||
const url = options.url.replace(/^https?:\/\/[^/]+/, base)
|
||||
uni.request({ ...options, url, success: (res) => {
|
||||
const { statusCode, data } = res
|
||||
if (statusCode >= 200 && statusCode < 300) return resolve(data)
|
||||
if (idx + 1 < candidates.length) return requestWithFallback(options, candidates, idx + 1, resolve, reject)
|
||||
reject(new Error('HTTP ' + statusCode))
|
||||
}, fail: (err) => {
|
||||
if (idx + 1 < candidates.length) return requestWithFallback(options, candidates, idx + 1, resolve, reject)
|
||||
reject(err)
|
||||
} })
|
||||
}
|
||||
|
||||
export function get(path, params = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const headers = { 'X-Shop-Id': SHOP_ID }
|
||||
if (ENABLE_DEFAULT_USER && DEFAULT_USER_ID) headers['X-User-Id'] = DEFAULT_USER_ID
|
||||
uni.request({
|
||||
url: buildUrl(path),
|
||||
method: 'GET',
|
||||
data: params,
|
||||
header: headers,
|
||||
success: (res) => {
|
||||
const { statusCode, data } = res
|
||||
if (statusCode >= 200 && statusCode < 300) return resolve(data)
|
||||
reject(new Error('HTTP ' + statusCode))
|
||||
},
|
||||
fail: (err) => reject(err)
|
||||
})
|
||||
const options = { url: buildUrl(path), method: 'GET', data: params, header: headers }
|
||||
requestWithFallback(options, API_BASE_URL_CANDIDATES, 0, resolve, reject)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -30,19 +34,63 @@ export function post(path, body = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const headers = { 'Content-Type': 'application/json', 'X-Shop-Id': SHOP_ID }
|
||||
if (ENABLE_DEFAULT_USER && DEFAULT_USER_ID) headers['X-User-Id'] = DEFAULT_USER_ID
|
||||
uni.request({
|
||||
url: buildUrl(path),
|
||||
method: 'POST',
|
||||
data: body,
|
||||
header: headers,
|
||||
success: (res) => {
|
||||
const { statusCode, data } = res
|
||||
if (statusCode >= 200 && statusCode < 300) return resolve(data)
|
||||
reject(new Error('HTTP ' + statusCode))
|
||||
},
|
||||
fail: (err) => reject(err)
|
||||
})
|
||||
const options = { url: buildUrl(path), method: 'POST', data: body, header: headers }
|
||||
requestWithFallback(options, API_BASE_URL_CANDIDATES, 0, resolve, reject)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
export function put(path, body = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const headers = { 'Content-Type': 'application/json', 'X-Shop-Id': SHOP_ID }
|
||||
if (ENABLE_DEFAULT_USER && DEFAULT_USER_ID) headers['X-User-Id'] = DEFAULT_USER_ID
|
||||
const options = { url: buildUrl(path), method: 'PUT', data: body, header: headers }
|
||||
requestWithFallback(options, API_BASE_URL_CANDIDATES, 0, resolve, reject)
|
||||
})
|
||||
}
|
||||
|
||||
export function del(path, body = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const headers = { 'Content-Type': 'application/json', 'X-Shop-Id': SHOP_ID }
|
||||
if (ENABLE_DEFAULT_USER && DEFAULT_USER_ID) headers['X-User-Id'] = DEFAULT_USER_ID
|
||||
const options = { url: buildUrl(path), method: 'DELETE', data: body, header: headers }
|
||||
requestWithFallback(options, API_BASE_URL_CANDIDATES, 0, resolve, reject)
|
||||
})
|
||||
}
|
||||
|
||||
function uploadWithFallback(options, candidates, idx, resolve, reject) {
|
||||
const base = candidates[idx] || API_BASE_URL
|
||||
const url = options.url.replace(/^https?:\/\/[^/]+/, base)
|
||||
const uploadOptions = { ...options, url }
|
||||
uni.uploadFile({
|
||||
...uploadOptions,
|
||||
success: (res) => {
|
||||
const statusCode = res.statusCode || 0
|
||||
if (statusCode >= 200 && statusCode < 300) {
|
||||
try {
|
||||
const data = typeof res.data === 'string' ? JSON.parse(res.data) : res.data
|
||||
return resolve(data)
|
||||
} catch (e) {
|
||||
return resolve(res.data)
|
||||
}
|
||||
}
|
||||
if (idx + 1 < candidates.length) return uploadWithFallback(options, candidates, idx + 1, resolve, reject)
|
||||
reject(new Error('HTTP ' + statusCode))
|
||||
},
|
||||
fail: (err) => {
|
||||
if (idx + 1 < candidates.length) return uploadWithFallback(options, candidates, idx + 1, resolve, reject)
|
||||
reject(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 文件上传封装:自动注入租户/用户头并进行多地址回退
|
||||
export function upload(path, filePath, formData = {}, name = 'file') {
|
||||
return new Promise((resolve, reject) => {
|
||||
const header = { 'X-Shop-Id': SHOP_ID }
|
||||
if (ENABLE_DEFAULT_USER && DEFAULT_USER_ID) header['X-User-Id'] = DEFAULT_USER_ID
|
||||
const options = { url: buildUrl(path), filePath, name, formData, header }
|
||||
uploadWithFallback(options, API_BASE_URL_CANDIDATES, 0, resolve, reject)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
164
frontend/components/ImageUploader.vue
Normal file
164
frontend/components/ImageUploader.vue
Normal file
@@ -0,0 +1,164 @@
|
||||
<template>
|
||||
<view class="uploader">
|
||||
<view class="grid" :style="{height: areaHeight+'rpx'}">
|
||||
<movable-area class="area" :style="{height: areaHeight+'rpx'}">
|
||||
<movable-view
|
||||
v-for="(img, index) in innerList"
|
||||
:key="img.uid"
|
||||
class="cell"
|
||||
:style="cellStyle(index)"
|
||||
:direction="'all'"
|
||||
:damping="40"
|
||||
:friction="2"
|
||||
:x="img.x"
|
||||
:y="img.y"
|
||||
@change="onMoving(index, $event)"
|
||||
@touchend="onMoveEnd(index)"
|
||||
>
|
||||
<image :src="img.url" mode="aspectFill" class="thumb" @click="preview(index)" />
|
||||
<view class="remove" @click.stop="remove(index)">×</view>
|
||||
</movable-view>
|
||||
|
||||
<view v-if="innerList.length < max" class="adder" @click="choose">
|
||||
<text>+</text>
|
||||
</view>
|
||||
</movable-area>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { upload } from '../common/http.js'
|
||||
|
||||
const ITEM_SIZE = 210 // rpx
|
||||
const GAP = 18 // rpx
|
||||
const COLS = 3
|
||||
|
||||
function px(rpx) {
|
||||
// 以 750 设计稿计算;此函数仅用于内部位置计算,不写入样式
|
||||
return rpx
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'ImageUploader',
|
||||
props: {
|
||||
modelValue: { type: Array, default: () => [] },
|
||||
max: { type: Number, default: 9 },
|
||||
uploadPath: { type: String, default: '/api/attachments' },
|
||||
uploadFieldName: { type: String, default: 'file' },
|
||||
formData: { type: Object, default: () => ({ ownerType: 'product' }) }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
innerList: []
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
areaHeight() {
|
||||
const rows = Math.ceil((this.innerList.length + 1) / COLS) || 1
|
||||
return rows * ITEM_SIZE + (rows - 1) * GAP
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
modelValue: {
|
||||
immediate: true,
|
||||
handler(list) {
|
||||
const mapped = (list || []).map((u, i) => ({
|
||||
uid: String(i) + '_' + (u.id || u.url || Math.random().toString(36).slice(2)),
|
||||
url: typeof u === 'string' ? u : (u.url || ''),
|
||||
x: this.posOf(i).x,
|
||||
y: this.posOf(i).y
|
||||
}))
|
||||
this.innerList = mapped
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
posOf(index) {
|
||||
const row = Math.floor(index / COLS)
|
||||
const col = index % COLS
|
||||
return { x: px(col * (ITEM_SIZE + GAP)), y: px(row * (ITEM_SIZE + GAP)) }
|
||||
},
|
||||
cellStyle(index) {
|
||||
return {
|
||||
width: ITEM_SIZE + 'rpx',
|
||||
height: ITEM_SIZE + 'rpx'
|
||||
}
|
||||
},
|
||||
preview(index) {
|
||||
uni.previewImage({ urls: this.innerList.map(i => i.url), current: index })
|
||||
},
|
||||
remove(index) {
|
||||
this.innerList.splice(index, 1)
|
||||
this.reflow()
|
||||
this.emit()
|
||||
},
|
||||
choose() {
|
||||
const remain = this.max - this.innerList.length
|
||||
if (remain <= 0) return
|
||||
uni.chooseImage({ count: remain, success: async (res) => {
|
||||
for (const path of res.tempFilePaths) {
|
||||
await this.doUpload(path)
|
||||
}
|
||||
}})
|
||||
},
|
||||
async doUpload(filePath) {
|
||||
try {
|
||||
const resp = await upload(this.uploadPath, filePath, this.formData, this.uploadFieldName)
|
||||
const url = resp?.url || resp?.data?.url || resp?.path || ''
|
||||
if (!url) throw new Error('上传响应无 url')
|
||||
this.innerList.push({ uid: Math.random().toString(36).slice(2), url, ...this.posOf(this.innerList.length) })
|
||||
this.reflow()
|
||||
this.emit()
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '上传失败', icon: 'none' })
|
||||
}
|
||||
},
|
||||
onMoving(index, e) {
|
||||
// 实时更新移动中元素的位置
|
||||
const { x, y } = e.detail
|
||||
this.innerList[index].x = x
|
||||
this.innerList[index].y = y
|
||||
},
|
||||
onMoveEnd(index) {
|
||||
// 根据落点推算新的索引
|
||||
const mv = this.innerList[index]
|
||||
const col = Math.round(mv.x / (ITEM_SIZE + GAP))
|
||||
const row = Math.round(mv.y / (ITEM_SIZE + GAP))
|
||||
let newIndex = row * COLS + col
|
||||
newIndex = Math.max(0, Math.min(newIndex, this.innerList.length - 1))
|
||||
if (newIndex !== index) {
|
||||
const moved = this.innerList.splice(index, 1)[0]
|
||||
this.innerList.splice(newIndex, 0, moved)
|
||||
}
|
||||
this.reflow()
|
||||
this.emit()
|
||||
},
|
||||
reflow() {
|
||||
this.innerList.forEach((it, i) => {
|
||||
const p = this.posOf(i)
|
||||
it.x = p.x
|
||||
it.y = p.y
|
||||
})
|
||||
},
|
||||
emit() {
|
||||
this.$emit('update:modelValue', this.innerList.map(i => i.url))
|
||||
this.$emit('change', this.innerList.map(i => i.url))
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.uploader { padding: 12rpx; background: #fff; }
|
||||
.grid { position: relative; }
|
||||
.area { width: 100%; position: relative; }
|
||||
.cell { position: absolute; border-radius: 12rpx; overflow: hidden; box-shadow: 0 0 1rpx rgba(0,0,0,0.08); }
|
||||
.thumb { width: 100%; height: 100%; }
|
||||
.remove { position: absolute; right: 6rpx; top: 6rpx; background: rgba(0,0,0,0.45); color: #fff; width: 40rpx; height: 40rpx; text-align: center; line-height: 40rpx; border-radius: 20rpx; font-size: 28rpx; }
|
||||
.adder { width: 210rpx; height: 210rpx; border: 2rpx dashed #ccc; border-radius: 12rpx; display: flex; align-items: center; justify-content: center; color: #999; position: absolute; left: 0; top: 0; }
|
||||
</style>
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -18,6 +18,36 @@
|
||||
"navigationBarTitleText": "选择商品"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/product/list",
|
||||
"style": {
|
||||
"navigationBarTitleText": "货品列表"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/product/form",
|
||||
"style": {
|
||||
"navigationBarTitleText": "编辑货品"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/product/categories",
|
||||
"style": {
|
||||
"navigationBarTitleText": "类别管理"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/product/units",
|
||||
"style": {
|
||||
"navigationBarTitleText": "单位管理"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/product/settings",
|
||||
"style": {
|
||||
"navigationBarTitleText": "货品设置"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/customer/select",
|
||||
"style": {
|
||||
|
||||
@@ -43,3 +43,4 @@
|
||||
</style>
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -48,3 +48,4 @@
|
||||
</style>
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -69,7 +69,7 @@
|
||||
<view class="tab" :class="{ active: activeTab==='home' }" @click="activeTab='home'">
|
||||
<text>首页</text>
|
||||
</view>
|
||||
<view class="tab" :class="{ active: activeTab==='product' }" @click="activeTab='product'">
|
||||
<view class="tab" :class="{ active: activeTab==='product' }" @click="goProduct">
|
||||
<text>货品</text>
|
||||
</view>
|
||||
<view class="tab primary" @click="onCreateOrder">
|
||||
@@ -99,6 +99,7 @@
|
||||
loadingNotices: false,
|
||||
noticeError: '',
|
||||
features: [
|
||||
{ key: 'product', title: '货品', img: '/static/icons/product.png', emoji: '📦' },
|
||||
{ key: 'customer', title: '客户', img: '/static/icons/customer.png', emoji: '👥' },
|
||||
{ key: 'sale', title: '销售', img: '/static/icons/sale.png', emoji: '💰' },
|
||||
{ key: 'account', title: '账户', img: '/static/icons/account.png', emoji: '💳' },
|
||||
@@ -118,12 +119,14 @@
|
||||
methods: {
|
||||
async fetchMetrics() {
|
||||
try {
|
||||
const d = await get('/api/metrics/overview')
|
||||
const d = await get('/api/dashboard/overview')
|
||||
const toNum = v => (typeof v === 'number' ? v : Number(v || 0))
|
||||
this.kpi = {
|
||||
todaySales: (d && d.todaySales) || '0.00',
|
||||
monthSales: (d && d.monthSales) || '0.00',
|
||||
monthProfit: (d && d.monthProfit) || '0.00',
|
||||
stockCount: (d && d.stockCount) || '0'
|
||||
...this.kpi,
|
||||
todaySales: toNum(d && d.todaySalesAmount).toFixed(2),
|
||||
monthSales: toNum(d && d.monthSalesAmount).toFixed(2),
|
||||
monthProfit: toNum(d && d.monthGrossProfit).toFixed(2),
|
||||
stockCount: String((d && d.stockTotalQuantity) != null ? d.stockTotalQuantity : 0)
|
||||
}
|
||||
} catch (e) {
|
||||
// 忽略错误,保留默认值
|
||||
@@ -145,8 +148,16 @@
|
||||
}
|
||||
},
|
||||
onFeatureTap(item) {
|
||||
if (item.key === 'product') {
|
||||
uni.navigateTo({ url: '/pages/product/list' })
|
||||
return
|
||||
}
|
||||
uni.showToast({ title: item.title + '(开发中)', icon: 'none' })
|
||||
},
|
||||
goProduct() {
|
||||
this.activeTab = 'product'
|
||||
uni.navigateTo({ url: '/pages/product/list' })
|
||||
},
|
||||
onCreateOrder() {
|
||||
uni.navigateTo({ url: '/pages/order/create' })
|
||||
},
|
||||
|
||||
67
frontend/pages/product/categories.vue
Normal file
67
frontend/pages/product/categories.vue
Normal file
@@ -0,0 +1,67 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<view class="toolbar">
|
||||
<input v-model.trim="name" placeholder="新类别名称" />
|
||||
<button size="mini" @click="create">新增</button>
|
||||
</view>
|
||||
<scroll-view scroll-y class="list">
|
||||
<view class="item" v-for="c in list" :key="c.id">
|
||||
<input v-model.trim="c.name" />
|
||||
<view class="ops">
|
||||
<button size="mini" @click="update(c)">保存</button>
|
||||
<button size="mini" type="warn" @click="remove(c)">删除</button>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { get, post, put, del } from '../../common/http.js'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return { name: '', list: [] }
|
||||
},
|
||||
onLoad() { this.reload() },
|
||||
methods: {
|
||||
async reload() {
|
||||
try {
|
||||
const res = await get('/api/product-categories')
|
||||
this.list = Array.isArray(res?.list) ? res.list : (Array.isArray(res) ? res : [])
|
||||
} catch (_) {}
|
||||
},
|
||||
async create() {
|
||||
if (!this.name) return
|
||||
await post('/api/product-categories', { name: this.name })
|
||||
this.name = ''
|
||||
this.reload()
|
||||
},
|
||||
async update(c) {
|
||||
await put('/api/product-categories/' + c.id, { name: c.name })
|
||||
uni.showToast({ title: '已保存', icon: 'success' })
|
||||
},
|
||||
async remove(c) {
|
||||
uni.showModal({ content: '确定删除该类别?', success: async (r) => {
|
||||
if (!r.confirm) return
|
||||
await del('/api/product-categories/' + c.id)
|
||||
this.reload()
|
||||
}})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.page { display:flex; flex-direction: column; height: 100vh; }
|
||||
.toolbar { display:flex; gap: 12rpx; padding: 16rpx; background:#fff; }
|
||||
.toolbar input { flex:1; background:#f6f6f6; border-radius: 12rpx; padding: 12rpx; }
|
||||
.list { flex:1; }
|
||||
.item { display:flex; gap: 12rpx; align-items:center; padding: 16rpx; background:#fff; border-bottom: 1rpx solid #f1f1f1; }
|
||||
.item input { flex:1; background:#f7f7f7; border-radius: 10rpx; padding: 12rpx; }
|
||||
.ops { display:flex; gap: 10rpx; }
|
||||
</style>
|
||||
|
||||
|
||||
|
||||
|
||||
213
frontend/pages/product/form.vue
Normal file
213
frontend/pages/product/form.vue
Normal file
@@ -0,0 +1,213 @@
|
||||
<template>
|
||||
<scroll-view scroll-y class="page">
|
||||
<view class="card">
|
||||
<view class="row">
|
||||
<text class="label">商品名称</text>
|
||||
<input v-model.trim="form.name" placeholder="必填" />
|
||||
</view>
|
||||
<view class="row">
|
||||
<text class="label">条形码</text>
|
||||
<input v-model.trim="form.barcode" placeholder="可扫码或输入" />
|
||||
<!-- #ifdef APP-PLUS -->
|
||||
<button size="mini" @click="scan">扫码</button>
|
||||
<!-- #endif -->
|
||||
</view>
|
||||
<view class="row">
|
||||
<text class="label">品牌/型号/规格/产地</text>
|
||||
</view>
|
||||
<view class="row">
|
||||
<input v-model.trim="form.brand" placeholder="品牌" />
|
||||
</view>
|
||||
<view class="row">
|
||||
<input v-model.trim="form.model" placeholder="型号" />
|
||||
</view>
|
||||
<view class="row">
|
||||
<input v-model.trim="form.spec" placeholder="规格" />
|
||||
</view>
|
||||
<view class="row">
|
||||
<input v-model.trim="form.origin" placeholder="产地" />
|
||||
</view>
|
||||
<view class="row">
|
||||
<picker mode="selector" :range="unitNames" @change="onPickUnit">
|
||||
<view class="picker">主单位:{{ unitLabel }}</view>
|
||||
</picker>
|
||||
<picker mode="selector" :range="categoryNames" @change="onPickCategory">
|
||||
<view class="picker">类别:{{ categoryLabel }}</view>
|
||||
</picker>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="card">
|
||||
<view class="row">
|
||||
<text class="label">库存与安全库存</text>
|
||||
</view>
|
||||
<view class="row">
|
||||
<input type="number" v-model.number="form.stock" placeholder="当前库存" />
|
||||
<input type="number" v-model.number="form.safeMin" placeholder="安全库存下限" />
|
||||
<input type="number" v-model.number="form.safeMax" placeholder="安全库存上限" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="card">
|
||||
<view class="row">
|
||||
<text class="label">价格(进价/零售/批发/大单)</text>
|
||||
</view>
|
||||
<view class="row prices">
|
||||
<input type="number" v-model.number="form.purchasePrice" placeholder="进货价" />
|
||||
<input type="number" v-model.number="form.retailPrice" placeholder="零售价" />
|
||||
<input type="number" v-model.number="form.wholesalePrice" placeholder="批发价" />
|
||||
<input type="number" v-model.number="form.bigClientPrice" placeholder="大单价" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="card">
|
||||
<text class="label">图片</text>
|
||||
<ImageUploader v-model="form.images" :formData="{ ownerType: 'product' }" />
|
||||
</view>
|
||||
|
||||
<view class="card">
|
||||
<text class="label">备注</text>
|
||||
<textarea v-model.trim="form.remark" placeholder="可选" auto-height />
|
||||
</view>
|
||||
|
||||
<view class="fixed">
|
||||
<button type="default" @click="save(false)">保存</button>
|
||||
<button type="primary" @click="save(true)">保存并继续</button>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ImageUploader from '../../components/ImageUploader.vue'
|
||||
import { get, post, put } from '../../common/http.js'
|
||||
|
||||
export default {
|
||||
components: { ImageUploader },
|
||||
data() {
|
||||
return {
|
||||
id: '',
|
||||
form: {
|
||||
name: '', barcode: '', brand: '', model: '', spec: '', origin: '',
|
||||
categoryId: '', unitId: '',
|
||||
stock: null, safeMin: null, safeMax: null,
|
||||
purchasePrice: null, retailPrice: null, wholesalePrice: null, bigClientPrice: null,
|
||||
images: [], remark: ''
|
||||
},
|
||||
units: [],
|
||||
categories: []
|
||||
}
|
||||
},
|
||||
onLoad(query) {
|
||||
this.id = query?.id || ''
|
||||
this.bootstrap()
|
||||
},
|
||||
computed: {
|
||||
unitNames() { return this.units.map(u => u.name) },
|
||||
categoryNames() { return this.categories.map(c => c.name) },
|
||||
unitLabel() {
|
||||
const u = this.units.find(x => String(x.id) === String(this.form.unitId))
|
||||
return u ? u.name : '选择单位'
|
||||
},
|
||||
categoryLabel() {
|
||||
const c = this.categories.find(x => String(x.id) === String(this.form.categoryId))
|
||||
return c ? c.name : '选择类别'
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async bootstrap() {
|
||||
await Promise.all([this.fetchUnits(), this.fetchCategories()])
|
||||
if (this.id) this.loadDetail()
|
||||
},
|
||||
async fetchUnits() {
|
||||
try {
|
||||
const res = await get('/api/product-units')
|
||||
this.units = Array.isArray(res?.list) ? res.list : (Array.isArray(res) ? res : [])
|
||||
} catch (_) {}
|
||||
},
|
||||
async fetchCategories() {
|
||||
try {
|
||||
const res = await get('/api/product-categories')
|
||||
this.categories = Array.isArray(res?.list) ? res.list : (Array.isArray(res) ? res : [])
|
||||
} catch (_) {}
|
||||
},
|
||||
onPickUnit(e) {
|
||||
const idx = Number(e.detail.value); const u = this.units[idx]
|
||||
this.form.unitId = u ? u.id : ''
|
||||
},
|
||||
onPickCategory(e) {
|
||||
const idx = Number(e.detail.value); const c = this.categories[idx]
|
||||
this.form.categoryId = c ? c.id : ''
|
||||
},
|
||||
scan() {
|
||||
uni.scanCode({ onlyFromCamera: false, success: (res) => {
|
||||
this.form.barcode = res.result
|
||||
}})
|
||||
},
|
||||
async loadDetail() {
|
||||
try {
|
||||
const data = await get('/api/products/' + this.id)
|
||||
Object.assign(this.form, {
|
||||
name: data.name,
|
||||
barcode: data.barcode, brand: data.brand, model: data.model, spec: data.spec, origin: data.origin,
|
||||
categoryId: data.categoryId, unitId: data.unitId,
|
||||
stock: data.stock,
|
||||
safeMin: data.safeMin, safeMax: data.safeMax,
|
||||
purchasePrice: data.purchasePrice, retailPrice: data.retailPrice,
|
||||
wholesalePrice: data.wholesalePrice, bigClientPrice: data.bigClientPrice,
|
||||
images: (data.images || []).map(i => i.url || i)
|
||||
})
|
||||
} catch (_) {}
|
||||
},
|
||||
validate() {
|
||||
if (!this.form.name) { uni.showToast({ title: '请填写名称', icon: 'none' }); return false }
|
||||
if (this.form.safeMin != null && this.form.safeMax != null && Number(this.form.safeMin) > Number(this.form.safeMax)) {
|
||||
uni.showToast({ title: '安全库存区间不合法', icon: 'none' }); return false
|
||||
}
|
||||
return true
|
||||
},
|
||||
buildPayload() {
|
||||
const f = this.form
|
||||
return {
|
||||
name: f.name, barcode: f.barcode, brand: f.brand, model: f.model, spec: f.spec, origin: f.origin,
|
||||
categoryId: f.categoryId || null, unitId: f.unitId,
|
||||
safeMin: f.safeMin, safeMax: f.safeMax,
|
||||
prices: {
|
||||
purchasePrice: f.purchasePrice, retailPrice: f.retailPrice, wholesalePrice: f.wholesalePrice, bigClientPrice: f.bigClientPrice
|
||||
},
|
||||
stock: f.stock,
|
||||
images: f.images,
|
||||
remark: f.remark
|
||||
}
|
||||
},
|
||||
async save(goOn) {
|
||||
if (!this.validate()) return
|
||||
const payload = this.buildPayload()
|
||||
try {
|
||||
if (this.id) await put('/api/products/' + this.id, payload)
|
||||
else await post('/api/products', payload)
|
||||
uni.showToast({ title: '保存成功', icon: 'success' })
|
||||
if (goOn && !this.id) {
|
||||
this.form = { name: '', barcode: '', brand: '', model: '', spec: '', origin: '', categoryId: '', unitId: '', stock: null, safeMin: null, safeMax: null, purchasePrice: null, retailPrice: null, wholesalePrice: null, bigClientPrice: null, images: [], remark: '' }
|
||||
} else {
|
||||
setTimeout(() => uni.navigateBack(), 400)
|
||||
}
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '保存失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.page { background:#f6f6f6; height: 100vh; }
|
||||
.card { background:#fff; margin: 16rpx; padding: 16rpx; border-radius: 12rpx; }
|
||||
.row { display:flex; gap: 12rpx; align-items: center; margin-bottom: 12rpx; }
|
||||
.label { width: 180rpx; color:#666; }
|
||||
.row input { flex:1; background:#f7f7f7; border-radius: 10rpx; padding: 12rpx; }
|
||||
.picker { padding: 8rpx 12rpx; background:#f0f0f0; border-radius: 10rpx; color:#666; margin-left: 8rpx; }
|
||||
.prices input { width: 30%; }
|
||||
.fixed { position: fixed; left: 0; right: 0; bottom: 0; background:#fff; padding: 12rpx 16rpx; display:flex; gap: 16rpx; }
|
||||
</style>
|
||||
|
||||
|
||||
133
frontend/pages/product/list.vue
Normal file
133
frontend/pages/product/list.vue
Normal file
@@ -0,0 +1,133 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<view class="tabs">
|
||||
<view class="tab" :class="{active: tab==='all'}" @click="switchTab('all')">全部</view>
|
||||
<view class="tab" :class="{active: tab==='category'}" @click="switchTab('category')">按类别</view>
|
||||
</view>
|
||||
|
||||
<view class="search">
|
||||
<input v-model.trim="query.kw" placeholder="输入名称/条码/规格查询" @confirm="reload" />
|
||||
<picker mode="selector" :range="categoryNames" v-if="tab==='category'" @change="onPickCategory">
|
||||
<view class="picker">{{ categoryLabel }}</view>
|
||||
</picker>
|
||||
<button size="mini" @click="reload">查询</button>
|
||||
</view>
|
||||
|
||||
<scroll-view scroll-y class="list" @scrolltolower="loadMore">
|
||||
<block v-if="items.length">
|
||||
<view class="item" v-for="it in items" :key="it.id" @click="openForm(it.id)">
|
||||
<image v-if="it.cover" :src="it.cover" class="thumb" mode="aspectFill" />
|
||||
<view class="content">
|
||||
<view class="name">{{ it.name }}</view>
|
||||
<view class="meta">{{ it.brand || '-' }} {{ it.model || '' }} {{ it.spec || '' }}</view>
|
||||
<view class="meta">库存:{{ it.stock ?? 0 }}
|
||||
<text class="price">零售价:¥{{ (it.retailPrice ?? it.price ?? 0).toFixed(2) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
<view v-else class="empty">
|
||||
<text>暂无数据,点击右上角“+”新增</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<view class="fab" @click="openForm()">+</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { get } from '../../common/http.js'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
items: [],
|
||||
query: { kw: '', page: 1, size: 20, categoryId: '' },
|
||||
finished: false,
|
||||
loading: false,
|
||||
tab: 'all',
|
||||
categories: []
|
||||
}
|
||||
},
|
||||
onLoad() {
|
||||
this.fetchCategories()
|
||||
this.reload()
|
||||
},
|
||||
computed: {
|
||||
categoryNames() { return this.categories.map(c => c.name) },
|
||||
categoryLabel() {
|
||||
const c = this.categories.find(x => String(x.id) === String(this.query.categoryId))
|
||||
return c ? '类别:' + c.name : '选择类别'
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
switchTab(t) {
|
||||
this.tab = t
|
||||
this.query.categoryId = ''
|
||||
this.reload()
|
||||
},
|
||||
onPickCategory(e) {
|
||||
const idx = Number(e.detail.value)
|
||||
const c = this.categories[idx]
|
||||
this.query.categoryId = c ? c.id : ''
|
||||
this.reload()
|
||||
},
|
||||
async fetchCategories() {
|
||||
try {
|
||||
const res = await get('/api/product-categories', {})
|
||||
this.categories = Array.isArray(res?.list) ? res.list : (Array.isArray(res) ? res : [])
|
||||
} catch (_) {}
|
||||
},
|
||||
reload() {
|
||||
this.items = []
|
||||
this.query.page = 1
|
||||
this.finished = false
|
||||
this.loadMore()
|
||||
},
|
||||
async loadMore() {
|
||||
if (this.loading || this.finished) return
|
||||
this.loading = true
|
||||
try {
|
||||
const params = { kw: this.query.kw, page: this.query.page, size: this.query.size }
|
||||
if (this.tab === 'category' && this.query.categoryId) params.categoryId = this.query.categoryId
|
||||
const res = await get('/api/products', params)
|
||||
const list = Array.isArray(res?.list) ? res.list : (Array.isArray(res) ? res : [])
|
||||
this.items = this.items.concat(list)
|
||||
if (list.length < this.query.size) this.finished = true
|
||||
this.query.page += 1
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '加载失败', icon: 'none' })
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
openForm(id) {
|
||||
const url = '/pages/product/form' + (id ? ('?id=' + id) : '')
|
||||
uni.navigateTo({ url })
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.page { display:flex; flex-direction: column; height: 100vh; }
|
||||
.tabs { display:flex; background:#fff; }
|
||||
.tab { flex:1; text-align:center; padding: 20rpx 0; color:#666; }
|
||||
.tab.active { color:#18b566; font-weight: 600; }
|
||||
.search { display:flex; gap: 12rpx; padding: 16rpx; background:#fff; align-items: center; }
|
||||
.search input { flex:1; background:#f6f6f6; border-radius: 12rpx; padding: 12rpx; }
|
||||
.picker { padding: 8rpx 12rpx; background:#f0f0f0; border-radius: 10rpx; color:#666; }
|
||||
.list { flex:1; }
|
||||
.item { display:flex; padding: 20rpx; background:#fff; border-bottom: 1rpx solid #f1f1f1; }
|
||||
.thumb { width: 120rpx; height: 120rpx; border-radius: 12rpx; margin-right: 16rpx; background:#fafafa; }
|
||||
.content { flex:1; }
|
||||
.name { color:#333; margin-bottom: 6rpx; font-weight: 600; }
|
||||
.meta { color:#888; font-size: 24rpx; }
|
||||
.price { margin-left: 20rpx; color:#f60; }
|
||||
.empty { height: 60vh; display:flex; align-items:center; justify-content:center; color:#999; }
|
||||
.fab { position: fixed; right: 30rpx; bottom: 120rpx; width: 100rpx; height: 100rpx; background:#18b566; color:#fff; border-radius: 50rpx; text-align:center; line-height: 100rpx; font-size: 48rpx; box-shadow: 0 8rpx 20rpx rgba(0,0,0,0.15); }
|
||||
</style>
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -47,3 +47,4 @@
|
||||
</style>
|
||||
|
||||
|
||||
|
||||
|
||||
45
frontend/pages/product/settings.vue
Normal file
45
frontend/pages/product/settings.vue
Normal file
@@ -0,0 +1,45 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<view class="item">
|
||||
<text>隐藏零库存商品</text>
|
||||
<switch :checked="settings.hideZeroStock" @change="(e)=>update('hideZeroStock', e.detail.value)" />
|
||||
</view>
|
||||
<view class="item">
|
||||
<text>隐藏进货价</text>
|
||||
<switch :checked="settings.hidePurchasePrice" @change="(e)=>update('hidePurchasePrice', e.detail.value)" />
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { get, put } from '../../common/http.js'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return { settings: { hideZeroStock: false, hidePurchasePrice: false } }
|
||||
},
|
||||
onLoad() { this.load() },
|
||||
methods: {
|
||||
async load() {
|
||||
try {
|
||||
const res = await get('/api/product-settings')
|
||||
this.settings = { hideZeroStock: !!res?.hideZeroStock, hidePurchasePrice: !!res?.hidePurchasePrice }
|
||||
} catch (_) {}
|
||||
},
|
||||
async update(key, val) {
|
||||
const next = { ...this.settings, [key]: val }
|
||||
this.settings = next
|
||||
try { await put('/api/product-settings', next) } catch (_) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.page { background:#fff; }
|
||||
.item { display:flex; justify-content: space-between; align-items:center; padding: 20rpx; border-bottom: 1rpx solid #f1f1f1; }
|
||||
</style>
|
||||
|
||||
|
||||
|
||||
|
||||
67
frontend/pages/product/units.vue
Normal file
67
frontend/pages/product/units.vue
Normal file
@@ -0,0 +1,67 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<view class="toolbar">
|
||||
<input v-model.trim="name" placeholder="新单位名称" />
|
||||
<button size="mini" @click="create">新增</button>
|
||||
</view>
|
||||
<scroll-view scroll-y class="list">
|
||||
<view class="item" v-for="u in list" :key="u.id">
|
||||
<input v-model.trim="u.name" />
|
||||
<view class="ops">
|
||||
<button size="mini" @click="update(u)">保存</button>
|
||||
<button size="mini" type="warn" @click="remove(u)">删除</button>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { get, post, put, del } from '../../common/http.js'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return { name: '', list: [] }
|
||||
},
|
||||
onLoad() { this.reload() },
|
||||
methods: {
|
||||
async reload() {
|
||||
try {
|
||||
const res = await get('/api/product-units')
|
||||
this.list = Array.isArray(res?.list) ? res.list : (Array.isArray(res) ? res : [])
|
||||
} catch (_) {}
|
||||
},
|
||||
async create() {
|
||||
if (!this.name) return
|
||||
await post('/api/product-units', { name: this.name })
|
||||
this.name = ''
|
||||
this.reload()
|
||||
},
|
||||
async update(u) {
|
||||
await put('/api/product-units/' + u.id, { name: u.name })
|
||||
uni.showToast({ title: '已保存', icon: 'success' })
|
||||
},
|
||||
async remove(u) {
|
||||
uni.showModal({ content: '确定删除该单位?', success: async (r) => {
|
||||
if (!r.confirm) return
|
||||
await del('/api/product-units/' + u.id)
|
||||
this.reload()
|
||||
}})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.page { display:flex; flex-direction: column; height: 100vh; }
|
||||
.toolbar { display:flex; gap: 12rpx; padding: 16rpx; background:#fff; }
|
||||
.toolbar input { flex:1; background:#f6f6f6; border-radius: 12rpx; padding: 12rpx; }
|
||||
.list { flex:1; }
|
||||
.item { display:flex; gap: 12rpx; align-items:center; padding: 16rpx; background:#fff; border-bottom: 1rpx solid #f1f1f1; }
|
||||
.item input { flex:1; background:#f7f7f7; border-radius: 10rpx; padding: 12rpx; }
|
||||
.ops { display:flex; gap: 10rpx; }
|
||||
</style>
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -48,3 +48,4 @@
|
||||
</style>
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"version":3,"file":"app.js","sources":["App.vue","main.js"],"sourcesContent":["<script>\r\n\texport default {\r\n\t\tonLaunch: function() {\r\n\t\t\tconsole.log('App Launch')\r\n\t\t},\r\n\t\tonShow: function() {\r\n\t\t\tconsole.log('App Show')\r\n\t\t},\r\n\t\tonHide: function() {\r\n\t\t\tconsole.log('App Hide')\r\n\t\t}\r\n\t}\r\n</script>\r\n\r\n<style>\r\n\t/*每个页面公共css */\r\n</style>\r\n","import App from './App'\r\n\r\n// #ifndef VUE3\r\nimport Vue from 'vue'\r\nimport './uni.promisify.adaptor'\r\nVue.config.productionTip = false\r\nApp.mpType = 'app'\r\nconst app = new Vue({\r\n ...App\r\n})\r\napp.$mount()\r\n// #endif\r\n\r\n// #ifdef VUE3\r\nimport { createSSRApp } from 'vue'\r\nexport function createApp() {\r\n const app = createSSRApp(App)\r\n return {\r\n app\r\n }\r\n}\r\n// #endif"],"names":["uni","createSSRApp","App"],"mappings":";;;;;;;;;;;AACC,MAAK,YAAU;AAAA,EACd,UAAU,WAAW;AACpBA,kBAAAA,MAAA,MAAA,OAAA,gBAAY,YAAY;AAAA,EACxB;AAAA,EACD,QAAQ,WAAW;AAClBA,kBAAAA,MAAY,MAAA,OAAA,gBAAA,UAAU;AAAA,EACtB;AAAA,EACD,QAAQ,WAAW;AAClBA,kBAAAA,MAAY,MAAA,OAAA,iBAAA,UAAU;AAAA,EACvB;AACD;ACIM,SAAS,YAAY;AAC1B,QAAM,MAAMC,cAAY,aAACC,SAAG;AAC5B,SAAO;AAAA,IACL;AAAA,EACD;AACH;;;"}
|
||||
{"version":3,"file":"app.js","sources":["App.vue","main.js"],"sourcesContent":["<script>\r\n\texport default {\r\n\t\tonLaunch: function() {\r\n\t\t\tconsole.log('App Launch')\r\n\t\t},\r\n\t\tonShow: function() {\r\n\t\t\tconsole.log('App Show')\r\n\t\t},\r\n\t\tonHide: function() {\r\n\t\t\tconsole.log('App Hide')\r\n\t\t}\r\n\t}\r\n</script>\r\n\r\n<style>\r\n\t/*每个页面公共css */\r\n</style>\r\n","import App from './App'\r\n\r\n// #ifndef VUE3\r\nimport Vue from 'vue'\r\nimport './uni.promisify.adaptor'\r\nVue.config.productionTip = false\r\nApp.mpType = 'app'\r\nconst app = new Vue({\r\n ...App\r\n})\r\napp.$mount()\r\n// #endif\r\n\r\n// #ifdef VUE3\r\nimport { createSSRApp } from 'vue'\r\nexport function createApp() {\r\n const app = createSSRApp(App)\r\n return {\r\n app\r\n }\r\n}\r\n// #endif"],"names":["uni","createSSRApp","App"],"mappings":";;;;;;;;;;;;;;;;AACC,MAAK,YAAU;AAAA,EACd,UAAU,WAAW;AACpBA,kBAAAA,MAAA,MAAA,OAAA,gBAAY,YAAY;AAAA,EACxB;AAAA,EACD,QAAQ,WAAW;AAClBA,kBAAAA,MAAY,MAAA,OAAA,gBAAA,UAAU;AAAA,EACtB;AAAA,EACD,QAAQ,WAAW;AAClBA,kBAAAA,MAAY,MAAA,OAAA,iBAAA,UAAU;AAAA,EACvB;AACD;ACIM,SAAS,YAAY;AAC1B,QAAM,MAAMC,cAAY,aAACC,SAAG;AAC5B,SAAO;AAAA,IACL;AAAA,EACD;AACH;;;"}
|
||||
@@ -1 +1 @@
|
||||
{"version":3,"file":"config.js","sources":["common/config.js"],"sourcesContent":["// 统一配置:禁止在业务代码中硬编码\n// 优先级:环境变量(Vite/HBuilderX 构建注入) > 本地存储 > 默认值\n\nconst envBaseUrl = (typeof process !== 'undefined' && process.env && (process.env.VITE_APP_API_BASE_URL || process.env.API_BASE_URL)) || '';\nconst storageBaseUrl = typeof uni !== 'undefined' ? (uni.getStorageSync('API_BASE_URL') || '') : '';\nconst fallbackBaseUrl = 'http://localhost:8080';\n\nexport const API_BASE_URL = (envBaseUrl || storageBaseUrl || fallbackBaseUrl).replace(/\\/$/, '');\n\nconst envShopId = (typeof process !== 'undefined' && process.env && (process.env.VITE_APP_SHOP_ID || process.env.SHOP_ID)) || '';\nconst storageShopId = typeof uni !== 'undefined' ? (uni.getStorageSync('SHOP_ID') || '') : '';\nexport const SHOP_ID = Number(envShopId || storageShopId || 1);\n\n\n// 默认用户(可移除):\n// - 用途:开发/演示环境,自动将用户固定为“张老板”(id=2)\n// - 开关优先级:环境变量 > 本地存储 > 默认值\n// - 生产默认关闭(false);开发可通过本地存储或环境变量开启\nconst envEnableDefaultUser = (typeof process !== 'undefined' && process.env && (process.env.VITE_APP_ENABLE_DEFAULT_USER || process.env.ENABLE_DEFAULT_USER)) || '';\nconst storageEnableDefaultUser = typeof uni !== 'undefined' ? (uni.getStorageSync('ENABLE_DEFAULT_USER') || '') : '';\nexport const ENABLE_DEFAULT_USER = String(envEnableDefaultUser || storageEnableDefaultUser || 'false').toLowerCase() === 'true';\n\nconst envDefaultUserId = (typeof process !== 'undefined' && process.env && (process.env.VITE_APP_DEFAULT_USER_ID || process.env.DEFAULT_USER_ID)) || '';\nconst storageDefaultUserId = typeof uni !== 'undefined' ? (uni.getStorageSync('DEFAULT_USER_ID') || '') : '';\nexport const DEFAULT_USER_ID = Number(envDefaultUserId || storageDefaultUserId || 2);\n\n\n"],"names":["uni"],"mappings":";;AAGA,MAAM,aAAc,OAAO,YAAY,eAAe,QAAQ,QAAQ,QAAQ,IAAI,yBAAyB,QAAQ,IAAI,iBAAkB;AACzI,MAAM,iBAAiB,OAAOA,cAAG,UAAK,cAAeA,cAAAA,MAAI,eAAe,cAAc,KAAK,KAAM;AACjG,MAAM,kBAAkB;AAEZ,MAAC,gBAAgB,cAAc,kBAAkB,iBAAiB,QAAQ,OAAO,EAAE;AAE/F,MAAM,YAAa,OAAO,YAAY,eAAe,QAAQ,QAAQ,QAAQ,IAAI,oBAAoB,QAAQ,IAAI,YAAa;AAC9H,MAAM,gBAAgB,OAAOA,cAAG,UAAK,cAAeA,cAAAA,MAAI,eAAe,SAAS,KAAK,KAAM;AAC/E,MAAC,UAAU,OAAO,aAAa,iBAAiB,CAAC;AAO7D,MAAM,uBAAwB,OAAO,YAAY,eAAe,QAAQ,QAAQ,QAAQ,IAAI,gCAAgC,QAAQ,IAAI,wBAAyB;AACjK,MAAM,2BAA2B,OAAOA,cAAG,UAAK,cAAeA,cAAAA,MAAI,eAAe,qBAAqB,KAAK,KAAM;AACtG,MAAC,sBAAsB,OAAO,wBAAwB,4BAA4B,OAAO,EAAE,YAAW,MAAO;AAEzH,MAAM,mBAAoB,OAAO,YAAY,eAAe,QAAQ,QAAQ,QAAQ,IAAI,4BAA4B,QAAQ,IAAI,oBAAqB;AACrJ,MAAM,uBAAuB,OAAOA,cAAG,UAAK,cAAeA,cAAAA,MAAI,eAAe,iBAAiB,KAAK,KAAM;AAC9F,MAAC,kBAAkB,OAAO,oBAAoB,wBAAwB,CAAC;;;;;"}
|
||||
{"version":3,"file":"config.js","sources":["common/config.js"],"sourcesContent":["// 统一配置:禁止在业务代码中硬编码\n// 优先级:环境变量(Vite/HBuilderX 构建注入) > 本地存储 > 默认值\n\nconst envBaseUrl = (typeof process !== 'undefined' && process.env && (process.env.VITE_APP_API_BASE_URL || process.env.API_BASE_URL)) || '';\nconst storageBaseUrl = typeof uni !== 'undefined' ? (uni.getStorageSync('API_BASE_URL') || '') : '';\nconst fallbackBaseUrl = 'http://192.168.31.193:8080';\n\nexport const API_BASE_URL = (envBaseUrl || storageBaseUrl || fallbackBaseUrl).replace(/\\/$/, '');\n\n// 多地址候选(按优先级顺序,自动去重与去尾斜杠)\nconst candidateBases = [envBaseUrl, storageBaseUrl, fallbackBaseUrl, 'http://127.0.0.1:8080', 'http://localhost:8080'];\nexport const API_BASE_URL_CANDIDATES = Array.from(new Set(candidateBases.filter(Boolean))).map(u => String(u).replace(/\\/$/, ''));\n\nconst envShopId = (typeof process !== 'undefined' && process.env && (process.env.VITE_APP_SHOP_ID || process.env.SHOP_ID)) || '';\nconst storageShopId = typeof uni !== 'undefined' ? (uni.getStorageSync('SHOP_ID') || '') : '';\nexport const SHOP_ID = Number(envShopId || storageShopId || 1);\n\n\n// 默认用户(可移除):\n// - 用途:开发/演示环境,自动将用户固定为“张老板”(id=2)\n// - 开关优先级:环境变量 > 本地存储 > 默认值\n// - 生产默认关闭(false);开发可通过本地存储或环境变量开启\nconst envEnableDefaultUser = (typeof process !== 'undefined' && process.env && (process.env.VITE_APP_ENABLE_DEFAULT_USER || process.env.ENABLE_DEFAULT_USER)) || '';\nconst storageEnableDefaultUser = typeof uni !== 'undefined' ? (uni.getStorageSync('ENABLE_DEFAULT_USER') || '') : '';\nexport const ENABLE_DEFAULT_USER = String(envEnableDefaultUser || storageEnableDefaultUser || 'true').toLowerCase() === 'true';\n\nconst envDefaultUserId = (typeof process !== 'undefined' && process.env && (process.env.VITE_APP_DEFAULT_USER_ID || process.env.DEFAULT_USER_ID)) || '';\nconst storageDefaultUserId = typeof uni !== 'undefined' ? (uni.getStorageSync('DEFAULT_USER_ID') || '') : '';\nexport const DEFAULT_USER_ID = Number(envDefaultUserId || storageDefaultUserId || 2);\n\n\n"],"names":["uni"],"mappings":";;AAGA,MAAM,aAAc,OAAO,YAAY,eAAe,QAAQ,QAAQ,QAAQ,IAAI,yBAAyB,QAAQ,IAAI,iBAAkB;AACzI,MAAM,iBAAiB,OAAOA,cAAG,UAAK,cAAeA,cAAAA,MAAI,eAAe,cAAc,KAAK,KAAM;AACjG,MAAM,kBAAkB;AAEZ,MAAC,gBAAgB,cAAc,kBAAkB,iBAAiB,QAAQ,OAAO,EAAE;AAG/F,MAAM,iBAAiB,CAAC,YAAY,gBAAgB,iBAAiB,yBAAyB,uBAAuB;AACzG,MAAC,0BAA0B,MAAM,KAAK,IAAI,IAAI,eAAe,OAAO,OAAO,CAAC,CAAC,EAAE,IAAI,OAAK,OAAO,CAAC,EAAE,QAAQ,OAAO,EAAE,CAAC;AAEhI,MAAM,YAAa,OAAO,YAAY,eAAe,QAAQ,QAAQ,QAAQ,IAAI,oBAAoB,QAAQ,IAAI,YAAa;AAC9H,MAAM,gBAAgB,OAAOA,cAAG,UAAK,cAAeA,cAAAA,MAAI,eAAe,SAAS,KAAK,KAAM;AAC/E,MAAC,UAAU,OAAO,aAAa,iBAAiB,CAAC;AAO7D,MAAM,uBAAwB,OAAO,YAAY,eAAe,QAAQ,QAAQ,QAAQ,IAAI,gCAAgC,QAAQ,IAAI,wBAAyB;AACjK,MAAM,2BAA2B,OAAOA,cAAG,UAAK,cAAeA,cAAAA,MAAI,eAAe,qBAAqB,KAAK,KAAM;AACtG,MAAC,sBAAsB,OAAO,wBAAwB,4BAA4B,MAAM,EAAE,YAAW,MAAO;AAExH,MAAM,mBAAoB,OAAO,YAAY,eAAe,QAAQ,QAAQ,QAAQ,IAAI,4BAA4B,QAAQ,IAAI,oBAAqB;AACrJ,MAAM,uBAAuB,OAAOA,cAAG,UAAK,cAAeA,cAAAA,MAAI,eAAe,iBAAiB,KAAK,KAAM;AAC9F,MAAC,kBAAkB,OAAO,oBAAoB,wBAAwB,CAAC;;;;;;"}
|
||||
@@ -1 +1 @@
|
||||
{"version":3,"file":"constants.js","sources":["common/constants.js"],"sourcesContent":["// 统一常量配置:其他收入/支出分类,禁止在业务中硬编码\r\nexport const INCOME_CATEGORIES = [\r\n\t{ key: 'sale_income', label: '销售收入' },\r\n\t{ key: 'operation_income', label: '经营所得' },\r\n\t{ key: 'interest_income', label: '利息收入' },\r\n\t{ key: 'investment_income', label: '投资收入' },\r\n\t{ key: 'other_income', label: '其它收入' }\r\n]\r\n\r\nexport const EXPENSE_CATEGORIES = [\r\n\t{ key: 'operation_expense', label: '经营支出' },\r\n\t{ key: 'office_supplies', label: '办公用品' },\r\n\t{ key: 'rent', label: '房租' },\r\n\t{ key: 'interest_expense', label: '利息支出' },\r\n\t{ key: 'other_expense', label: '其它支出' }\r\n]\r\n\r\n\r\n"],"names":[],"mappings":";AACY,MAAC,oBAAoB;AAAA,EAChC,EAAE,KAAK,eAAe,OAAO,OAAQ;AAAA,EACrC,EAAE,KAAK,oBAAoB,OAAO,OAAQ;AAAA,EAC1C,EAAE,KAAK,mBAAmB,OAAO,OAAQ;AAAA,EACzC,EAAE,KAAK,qBAAqB,OAAO,OAAQ;AAAA,EAC3C,EAAE,KAAK,gBAAgB,OAAO,OAAQ;AACvC;AAEY,MAAC,qBAAqB;AAAA,EACjC,EAAE,KAAK,qBAAqB,OAAO,OAAQ;AAAA,EAC3C,EAAE,KAAK,mBAAmB,OAAO,OAAQ;AAAA,EACzC,EAAE,KAAK,QAAQ,OAAO,KAAM;AAAA,EAC5B,EAAE,KAAK,oBAAoB,OAAO,OAAQ;AAAA,EAC1C,EAAE,KAAK,iBAAiB,OAAO,OAAQ;AACxC;;;"}
|
||||
{"version":3,"file":"constants.js","sources":["common/constants.js"],"sourcesContent":["// 统一常量配置:其他收入/支出分类,禁止在业务中硬编码\r\nexport const INCOME_CATEGORIES = [\r\n\t{ key: 'sale_income', label: '销售收入' },\r\n\t{ key: 'operation_income', label: '经营所得' },\r\n\t{ key: 'interest_income', label: '利息收入' },\r\n\t{ key: 'investment_income', label: '投资收入' },\r\n\t{ key: 'other_income', label: '其它收入' }\r\n]\r\n\r\nexport const EXPENSE_CATEGORIES = [\r\n\t{ key: 'operation_expense', label: '经营支出' },\r\n\t{ key: 'office_supplies', label: '办公用品' },\r\n\t{ key: 'rent', label: '房租' },\r\n\t{ key: 'interest_expense', label: '利息支出' },\r\n\t{ key: 'other_expense', label: '其它支出' }\r\n]\r\n\r\n\r\n\r\n"],"names":[],"mappings":";AACY,MAAC,oBAAoB;AAAA,EAChC,EAAE,KAAK,eAAe,OAAO,OAAQ;AAAA,EACrC,EAAE,KAAK,oBAAoB,OAAO,OAAQ;AAAA,EAC1C,EAAE,KAAK,mBAAmB,OAAO,OAAQ;AAAA,EACzC,EAAE,KAAK,qBAAqB,OAAO,OAAQ;AAAA,EAC3C,EAAE,KAAK,gBAAgB,OAAO,OAAQ;AACvC;AAEY,MAAC,qBAAqB;AAAA,EACjC,EAAE,KAAK,qBAAqB,OAAO,OAAQ;AAAA,EAC3C,EAAE,KAAK,mBAAmB,OAAO,OAAQ;AAAA,EACzC,EAAE,KAAK,QAAQ,OAAO,KAAM;AAAA,EAC5B,EAAE,KAAK,oBAAoB,OAAO,OAAQ;AAAA,EAC1C,EAAE,KAAK,iBAAiB,OAAO,OAAQ;AACxC;;;"}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
frontend/unpackage/dist/dev/.sourcemap/mp-weixin/components/ImageUploader.js.map
vendored
Normal file
1
frontend/unpackage/dist/dev/.sourcemap/mp-weixin/components/ImageUploader.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
||||
{"version":3,"file":"select.js","sources":["pages/account/select.vue","../../../../Downloads/HBuilderX.4.76.2025082103/HBuilderX/plugins/uniapp-cli-vite/uniPage:/cGFnZXMvYWNjb3VudC9zZWxlY3QudnVl"],"sourcesContent":["<template>\r\n\t<view class=\"page\">\r\n\t\t<scroll-view scroll-y class=\"list\">\r\n\t\t\t<view class=\"item\" v-for=\"a in accounts\" :key=\"a.id\" @click=\"select(a)\">\r\n\t\t\t\t<view class=\"name\">{{ a.name }}</view>\r\n\t\t\t\t<view class=\"meta\">{{ typeLabel(a.type) }} · 余额:{{ a.balance?.toFixed ? a.balance.toFixed(2) : a.balance }}</view>\r\n\t\t\t</view>\r\n\t\t</scroll-view>\r\n\t</view>\r\n</template>\r\n\r\n<script>\r\n\timport { get } from '../../common/http.js'\r\n\tconst TYPE_MAP = { cash: '现金', bank: '银行', alipay: '支付宝', wechat: '微信', other: '其他' }\r\n\texport default {\r\n\t\tdata() { return { accounts: [] } },\r\n\t\tasync onLoad() {\r\n\t\t\ttry {\r\n\t\t\t\tconst res = await get('/api/accounts')\r\n\t\t\t\tthis.accounts = Array.isArray(res) ? res : (res?.list || [])\r\n\t\t\t} catch(e) { uni.showToast({ title: '加载失败', icon: 'none' }) }\r\n\t\t},\r\n\t\tmethods: {\r\n\t\t\tselect(a) {\r\n\t\t\t\tconst opener = getCurrentPages()[getCurrentPages().length-2]\r\n\t\t\t\tif (opener && opener.$vm) {\r\n\t\t\t\t\topener.$vm.selectedAccountId = a.id\r\n\t\t\t\t\topener.$vm.selectedAccountName = a.name\r\n\t\t\t\t}\r\n\t\t\t\tuni.navigateBack()\r\n\t\t\t},\r\n\t\t\ttypeLabel(t) { return TYPE_MAP[t] || t }\r\n\t\t}\r\n\t}\r\n</script>\r\n\r\n<style>\r\n\t.page { display:flex; flex-direction: column; height: 100vh; }\r\n\t.list { flex:1; }\r\n\t.item { padding: 20rpx 24rpx; background:#fff; border-bottom: 1rpx solid #f1f1f1; }\r\n\t.name { color:#333; margin-bottom: 6rpx; }\r\n\t.meta { color:#888; font-size: 24rpx; }\r\n</style>\r\n\r\n\r\n","import MiniProgramPage from 'C:/Users/21826/Desktop/Wj/PartsInquiry/frontend/pages/account/select.vue'\nwx.createPage(MiniProgramPage)"],"names":["get","uni"],"mappings":";;;AAaC,MAAM,WAAW,EAAE,MAAM,MAAM,MAAM,MAAM,QAAQ,OAAO,QAAQ,MAAM,OAAO,KAAK;AACpF,MAAK,YAAU;AAAA,EACd,OAAO;AAAE,WAAO,EAAE,UAAU,CAAG,EAAA;AAAA,EAAG;AAAA,EAClC,MAAM,SAAS;AACd,QAAI;AACH,YAAM,MAAM,MAAMA,YAAG,IAAC,eAAe;AACrC,WAAK,WAAW,MAAM,QAAQ,GAAG,IAAI,OAAO,2BAAK,SAAQ;aAClD,GAAG;AAAEC,oBAAAA,MAAI,UAAU,EAAE,OAAO,QAAQ,MAAM,OAAO,CAAC;AAAA,IAAE;AAAA,EAC5D;AAAA,EACD,SAAS;AAAA,IACR,OAAO,GAAG;AACT,YAAM,SAAS,gBAAiB,EAAC,gBAAe,EAAG,SAAO,CAAC;AAC3D,UAAI,UAAU,OAAO,KAAK;AACzB,eAAO,IAAI,oBAAoB,EAAE;AACjC,eAAO,IAAI,sBAAsB,EAAE;AAAA,MACpC;AACAA,oBAAAA,MAAI,aAAa;AAAA,IACjB;AAAA,IACD,UAAU,GAAG;AAAE,aAAO,SAAS,CAAC,KAAK;AAAA,IAAE;AAAA,EACxC;AACD;;;;;;;;;;;;;;;;AChCD,GAAG,WAAW,eAAe;"}
|
||||
{"version":3,"file":"select.js","sources":["pages/account/select.vue","../../../../Downloads/HBuilderX.4.76.2025082103/HBuilderX/plugins/uniapp-cli-vite/uniPage:/cGFnZXMvYWNjb3VudC9zZWxlY3QudnVl"],"sourcesContent":["<template>\r\n\t<view class=\"page\">\r\n\t\t<scroll-view scroll-y class=\"list\">\r\n\t\t\t<view class=\"item\" v-for=\"a in accounts\" :key=\"a.id\" @click=\"select(a)\">\r\n\t\t\t\t<view class=\"name\">{{ a.name }}</view>\r\n\t\t\t\t<view class=\"meta\">{{ typeLabel(a.type) }} · 余额:{{ a.balance?.toFixed ? a.balance.toFixed(2) : a.balance }}</view>\r\n\t\t\t</view>\r\n\t\t</scroll-view>\r\n\t</view>\r\n</template>\r\n\r\n<script>\r\n\timport { get } from '../../common/http.js'\r\n\tconst TYPE_MAP = { cash: '现金', bank: '银行', alipay: '支付宝', wechat: '微信', other: '其他' }\r\n\texport default {\r\n\t\tdata() { return { accounts: [] } },\r\n\t\tasync onLoad() {\r\n\t\t\ttry {\r\n\t\t\t\tconst res = await get('/api/accounts')\r\n\t\t\t\tthis.accounts = Array.isArray(res) ? res : (res?.list || [])\r\n\t\t\t} catch(e) { uni.showToast({ title: '加载失败', icon: 'none' }) }\r\n\t\t},\r\n\t\tmethods: {\r\n\t\t\tselect(a) {\r\n\t\t\t\tconst opener = getCurrentPages()[getCurrentPages().length-2]\r\n\t\t\t\tif (opener && opener.$vm) {\r\n\t\t\t\t\topener.$vm.selectedAccountId = a.id\r\n\t\t\t\t\topener.$vm.selectedAccountName = a.name\r\n\t\t\t\t}\r\n\t\t\t\tuni.navigateBack()\r\n\t\t\t},\r\n\t\t\ttypeLabel(t) { return TYPE_MAP[t] || t }\r\n\t\t}\r\n\t}\r\n</script>\r\n\r\n<style>\r\n\t.page { display:flex; flex-direction: column; height: 100vh; }\r\n\t.list { flex:1; }\r\n\t.item { padding: 20rpx 24rpx; background:#fff; border-bottom: 1rpx solid #f1f1f1; }\r\n\t.name { color:#333; margin-bottom: 6rpx; }\r\n\t.meta { color:#888; font-size: 24rpx; }\r\n</style>\r\n\r\n\r\n\r\n","import MiniProgramPage from 'C:/Users/21826/Desktop/Wj/PartsInquiry/frontend/pages/account/select.vue'\nwx.createPage(MiniProgramPage)"],"names":["get","uni"],"mappings":";;;AAaC,MAAM,WAAW,EAAE,MAAM,MAAM,MAAM,MAAM,QAAQ,OAAO,QAAQ,MAAM,OAAO,KAAK;AACpF,MAAK,YAAU;AAAA,EACd,OAAO;AAAE,WAAO,EAAE,UAAU,CAAG,EAAA;AAAA,EAAG;AAAA,EAClC,MAAM,SAAS;AACd,QAAI;AACH,YAAM,MAAM,MAAMA,YAAG,IAAC,eAAe;AACrC,WAAK,WAAW,MAAM,QAAQ,GAAG,IAAI,OAAO,2BAAK,SAAQ;aAClD,GAAG;AAAEC,oBAAAA,MAAI,UAAU,EAAE,OAAO,QAAQ,MAAM,OAAO,CAAC;AAAA,IAAE;AAAA,EAC5D;AAAA,EACD,SAAS;AAAA,IACR,OAAO,GAAG;AACT,YAAM,SAAS,gBAAiB,EAAC,gBAAe,EAAG,SAAO,CAAC;AAC3D,UAAI,UAAU,OAAO,KAAK;AACzB,eAAO,IAAI,oBAAoB,EAAE;AACjC,eAAO,IAAI,sBAAsB,EAAE;AAAA,MACpC;AACAA,oBAAAA,MAAI,aAAa;AAAA,IACjB;AAAA,IACD,UAAU,GAAG;AAAE,aAAO,SAAS,CAAC,KAAK;AAAA,IAAE;AAAA,EACxC;AACD;;;;;;;;;;;;;;;;AChCD,GAAG,WAAW,eAAe;"}
|
||||
@@ -1 +1 @@
|
||||
{"version":3,"file":"select.js","sources":["pages/customer/select.vue","../../../../Downloads/HBuilderX.4.76.2025082103/HBuilderX/plugins/uniapp-cli-vite/uniPage:/cGFnZXMvY3VzdG9tZXIvc2VsZWN0LnZ1ZQ"],"sourcesContent":["<template>\r\n\t<view class=\"page\">\r\n\t\t<view class=\"search\">\r\n\t\t\t<input v-model=\"kw\" placeholder=\"搜索客户名称/电话\" @confirm=\"search\" />\r\n\t\t\t<button size=\"mini\" @click=\"search\">搜索</button>\r\n\t\t</view>\r\n\t\t<scroll-view scroll-y class=\"list\">\r\n\t\t\t<view class=\"item\" v-for=\"c in customers\" :key=\"c.id\" @click=\"select(c)\">\r\n\t\t\t\t<view class=\"name\">{{ c.name }}</view>\r\n\t\t\t\t<view class=\"meta\">{{ c.mobile || '—' }}</view>\r\n\t\t\t</view>\r\n\t\t</scroll-view>\r\n\t</view>\r\n</template>\r\n\r\n<script>\r\n\timport { get } from '../../common/http.js'\r\n\texport default {\r\n\t\tdata() { return { kw: '', customers: [] } },\r\n\t\tonLoad() { this.search() },\r\n\t\tmethods: {\r\n\t\t\tasync search() {\r\n\t\t\t\ttry {\r\n\t\t\t\t\tconst res = await get('/api/customers', { kw: this.kw, page: 1, size: 50 })\r\n\t\t\t\t\tthis.customers = Array.isArray(res?.list) ? res.list : (Array.isArray(res) ? res : [])\r\n\t\t\t\t} catch(e) { uni.showToast({ title: '加载失败', icon: 'none' }) }\r\n\t\t\t},\r\n\t\t\tselect(c) {\r\n\t\t\t\tconst opener = getCurrentPages()[getCurrentPages().length-2]\r\n\t\t\t\tif (opener && opener.$vm) {\r\n\t\t\t\t\topener.$vm.order.customerId = c.id\r\n\t\t\t\t\topener.$vm.customerName = c.name\r\n\t\t\t\t}\r\n\t\t\t\tuni.navigateBack()\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n</script>\r\n\r\n<style>\r\n\t.page { display:flex; flex-direction: column; height: 100vh; }\r\n\t.search { display:flex; gap: 12rpx; padding: 16rpx; background:#fff; }\r\n\t.search input { flex:1; background:#f6f6f6; border-radius: 12rpx; padding: 12rpx; }\r\n\t.list { flex:1; }\r\n\t.item { padding: 20rpx 24rpx; background:#fff; border-bottom: 1rpx solid #f1f1f1; }\r\n\t.name { color:#333; margin-bottom: 6rpx; }\r\n\t.meta { color:#888; font-size: 24rpx; }\r\n</style>\r\n\r\n\r\n","import MiniProgramPage from 'C:/Users/21826/Desktop/Wj/PartsInquiry/frontend/pages/customer/select.vue'\nwx.createPage(MiniProgramPage)"],"names":["get","uni"],"mappings":";;;AAiBC,MAAK,YAAU;AAAA,EACd,OAAO;AAAE,WAAO,EAAE,IAAI,IAAI,WAAW,CAAA;EAAM;AAAA,EAC3C,SAAS;AAAE,SAAK;EAAU;AAAA,EAC1B,SAAS;AAAA,IACR,MAAM,SAAS;AACd,UAAI;AACH,cAAM,MAAM,MAAMA,gBAAI,kBAAkB,EAAE,IAAI,KAAK,IAAI,MAAM,GAAG,MAAM,GAAC,CAAG;AAC1E,aAAK,YAAY,MAAM,QAAQ,2BAAK,IAAI,IAAI,IAAI,OAAQ,MAAM,QAAQ,GAAG,IAAI,MAAM,CAAA;AAAA,eAC5E,GAAG;AAAEC,sBAAAA,MAAI,UAAU,EAAE,OAAO,QAAQ,MAAM,OAAO,CAAC;AAAA,MAAE;AAAA,IAC5D;AAAA,IACD,OAAO,GAAG;AACT,YAAM,SAAS,gBAAiB,EAAC,gBAAe,EAAG,SAAO,CAAC;AAC3D,UAAI,UAAU,OAAO,KAAK;AACzB,eAAO,IAAI,MAAM,aAAa,EAAE;AAChC,eAAO,IAAI,eAAe,EAAE;AAAA,MAC7B;AACAA,oBAAAA,MAAI,aAAa;AAAA,IAClB;AAAA,EACD;AACD;;;;;;;;;;;;;;;;;;ACnCD,GAAG,WAAW,eAAe;"}
|
||||
{"version":3,"file":"select.js","sources":["pages/customer/select.vue","../../../../Downloads/HBuilderX.4.76.2025082103/HBuilderX/plugins/uniapp-cli-vite/uniPage:/cGFnZXMvY3VzdG9tZXIvc2VsZWN0LnZ1ZQ"],"sourcesContent":["<template>\r\n\t<view class=\"page\">\r\n\t\t<view class=\"search\">\r\n\t\t\t<input v-model=\"kw\" placeholder=\"搜索客户名称/电话\" @confirm=\"search\" />\r\n\t\t\t<button size=\"mini\" @click=\"search\">搜索</button>\r\n\t\t</view>\r\n\t\t<scroll-view scroll-y class=\"list\">\r\n\t\t\t<view class=\"item\" v-for=\"c in customers\" :key=\"c.id\" @click=\"select(c)\">\r\n\t\t\t\t<view class=\"name\">{{ c.name }}</view>\r\n\t\t\t\t<view class=\"meta\">{{ c.mobile || '—' }}</view>\r\n\t\t\t</view>\r\n\t\t</scroll-view>\r\n\t</view>\r\n</template>\r\n\r\n<script>\r\n\timport { get } from '../../common/http.js'\r\n\texport default {\r\n\t\tdata() { return { kw: '', customers: [] } },\r\n\t\tonLoad() { this.search() },\r\n\t\tmethods: {\r\n\t\t\tasync search() {\r\n\t\t\t\ttry {\r\n\t\t\t\t\tconst res = await get('/api/customers', { kw: this.kw, page: 1, size: 50 })\r\n\t\t\t\t\tthis.customers = Array.isArray(res?.list) ? res.list : (Array.isArray(res) ? res : [])\r\n\t\t\t\t} catch(e) { uni.showToast({ title: '加载失败', icon: 'none' }) }\r\n\t\t\t},\r\n\t\t\tselect(c) {\r\n\t\t\t\tconst opener = getCurrentPages()[getCurrentPages().length-2]\r\n\t\t\t\tif (opener && opener.$vm) {\r\n\t\t\t\t\topener.$vm.order.customerId = c.id\r\n\t\t\t\t\topener.$vm.customerName = c.name\r\n\t\t\t\t}\r\n\t\t\t\tuni.navigateBack()\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n</script>\r\n\r\n<style>\r\n\t.page { display:flex; flex-direction: column; height: 100vh; }\r\n\t.search { display:flex; gap: 12rpx; padding: 16rpx; background:#fff; }\r\n\t.search input { flex:1; background:#f6f6f6; border-radius: 12rpx; padding: 12rpx; }\r\n\t.list { flex:1; }\r\n\t.item { padding: 20rpx 24rpx; background:#fff; border-bottom: 1rpx solid #f1f1f1; }\r\n\t.name { color:#333; margin-bottom: 6rpx; }\r\n\t.meta { color:#888; font-size: 24rpx; }\r\n</style>\r\n\r\n\r\n\r\n","import MiniProgramPage from 'C:/Users/21826/Desktop/Wj/PartsInquiry/frontend/pages/customer/select.vue'\nwx.createPage(MiniProgramPage)"],"names":["get","uni"],"mappings":";;;AAiBC,MAAK,YAAU;AAAA,EACd,OAAO;AAAE,WAAO,EAAE,IAAI,IAAI,WAAW,CAAA;EAAM;AAAA,EAC3C,SAAS;AAAE,SAAK;EAAU;AAAA,EAC1B,SAAS;AAAA,IACR,MAAM,SAAS;AACd,UAAI;AACH,cAAM,MAAM,MAAMA,gBAAI,kBAAkB,EAAE,IAAI,KAAK,IAAI,MAAM,GAAG,MAAM,GAAC,CAAG;AAC1E,aAAK,YAAY,MAAM,QAAQ,2BAAK,IAAI,IAAI,IAAI,OAAQ,MAAM,QAAQ,GAAG,IAAI,MAAM,CAAA;AAAA,eAC5E,GAAG;AAAEC,sBAAAA,MAAI,UAAU,EAAE,OAAO,QAAQ,MAAM,OAAO,CAAC;AAAA,MAAE;AAAA,IAC5D;AAAA,IACD,OAAO,GAAG;AACT,YAAM,SAAS,gBAAiB,EAAC,gBAAe,EAAG,SAAO,CAAC;AAC3D,UAAI,UAAU,OAAO,KAAK;AACzB,eAAO,IAAI,MAAM,aAAa,EAAE;AAChC,eAAO,IAAI,eAAe,EAAE;AAAA,MAC7B;AACAA,oBAAAA,MAAI,aAAa;AAAA,IAClB;AAAA,EACD;AACD;;;;;;;;;;;;;;;;;;ACnCD,GAAG,WAAW,eAAe;"}
|
||||
File diff suppressed because one or more lines are too long
1
frontend/unpackage/dist/dev/.sourcemap/mp-weixin/pages/product/categories.js.map
vendored
Normal file
1
frontend/unpackage/dist/dev/.sourcemap/mp-weixin/pages/product/categories.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"categories.js","sources":["pages/product/categories.vue","../../../../Downloads/HBuilderX.4.76.2025082103/HBuilderX/plugins/uniapp-cli-vite/uniPage:/cGFnZXMvcHJvZHVjdC9jYXRlZ29yaWVzLnZ1ZQ"],"sourcesContent":["<template>\r\n\t<view class=\"page\">\r\n\t\t<view class=\"toolbar\">\r\n\t\t\t<input v-model.trim=\"name\" placeholder=\"新类别名称\" />\r\n\t\t\t<button size=\"mini\" @click=\"create\">新增</button>\r\n\t\t</view>\r\n\t\t<scroll-view scroll-y class=\"list\">\r\n\t\t\t<view class=\"item\" v-for=\"c in list\" :key=\"c.id\">\r\n\t\t\t\t<input v-model.trim=\"c.name\" />\r\n\t\t\t\t<view class=\"ops\">\r\n\t\t\t\t\t<button size=\"mini\" @click=\"update(c)\">保存</button>\r\n\t\t\t\t\t<button size=\"mini\" type=\"warn\" @click=\"remove(c)\">删除</button>\r\n\t\t\t\t</view>\r\n\t\t\t</view>\r\n\t\t</scroll-view>\r\n\t</view>\r\n</template>\r\n\r\n<script>\r\nimport { get, post, put, del } from '../../common/http.js'\r\n\r\nexport default {\r\n\tdata() {\r\n\t\treturn { name: '', list: [] }\r\n\t},\r\n\tonLoad() { this.reload() },\r\n\tmethods: {\r\n\t\tasync reload() {\r\n\t\t\ttry {\r\n\t\t\t\tconst res = await get('/api/product-categories')\r\n\t\t\t\tthis.list = Array.isArray(res?.list) ? res.list : (Array.isArray(res) ? res : [])\r\n\t\t\t} catch (_) {}\r\n\t\t},\r\n\t\tasync create() {\r\n\t\t\tif (!this.name) return\r\n\t\t\tawait post('/api/product-categories', { name: this.name })\r\n\t\t\tthis.name = ''\r\n\t\t\tthis.reload()\r\n\t\t},\r\n\t\tasync update(c) {\r\n\t\t\tawait put('/api/product-categories/' + c.id, { name: c.name })\r\n\t\t\tuni.showToast({ title: '已保存', icon: 'success' })\r\n\t\t},\r\n\t\tasync remove(c) {\r\n\t\t\tuni.showModal({ content: '确定删除该类别?', success: async (r) => {\r\n\t\t\t\tif (!r.confirm) return\r\n\t\t\t\tawait del('/api/product-categories/' + c.id)\r\n\t\t\t\tthis.reload()\r\n\t\t\t}})\r\n\t\t}\r\n\t}\r\n}\r\n</script>\r\n\r\n<style>\r\n.page { display:flex; flex-direction: column; height: 100vh; }\r\n.toolbar { display:flex; gap: 12rpx; padding: 16rpx; background:#fff; }\r\n.toolbar input { flex:1; background:#f6f6f6; border-radius: 12rpx; padding: 12rpx; }\r\n.list { flex:1; }\r\n.item { display:flex; gap: 12rpx; align-items:center; padding: 16rpx; background:#fff; border-bottom: 1rpx solid #f1f1f1; }\r\n.item input { flex:1; background:#f7f7f7; border-radius: 10rpx; padding: 12rpx; }\r\n.ops { display:flex; gap: 10rpx; }\r\n</style>\r\n\r\n\r\n\r\n\r\n","import MiniProgramPage from 'C:/Users/21826/Desktop/Wj/PartsInquiry/frontend/pages/product/categories.vue'\nwx.createPage(MiniProgramPage)"],"names":["get","post","put","uni","del"],"mappings":";;;AAqBA,MAAK,YAAU;AAAA,EACd,OAAO;AACN,WAAO,EAAE,MAAM,IAAI,MAAM,CAAA,EAAG;AAAA,EAC5B;AAAA,EACD,SAAS;AAAE,SAAK;EAAU;AAAA,EAC1B,SAAS;AAAA,IACR,MAAM,SAAS;AACd,UAAI;AACH,cAAM,MAAM,MAAMA,YAAG,IAAC,yBAAyB;AAC/C,aAAK,OAAO,MAAM,QAAQ,2BAAK,IAAI,IAAI,IAAI,OAAQ,MAAM,QAAQ,GAAG,IAAI,MAAM,CAAA;AAAA,eACtE,GAAG;AAAA,MAAC;AAAA,IACb;AAAA,IACD,MAAM,SAAS;AACd,UAAI,CAAC,KAAK;AAAM;AAChB,YAAMC,YAAAA,KAAK,2BAA2B,EAAE,MAAM,KAAK,KAAG,CAAG;AACzD,WAAK,OAAO;AACZ,WAAK,OAAO;AAAA,IACZ;AAAA,IACD,MAAM,OAAO,GAAG;AACf,YAAMC,YAAG,IAAC,6BAA6B,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM;AAC7DC,oBAAG,MAAC,UAAU,EAAE,OAAO,OAAO,MAAM,WAAW;AAAA,IAC/C;AAAA,IACD,MAAM,OAAO,GAAG;AACfA,oBAAG,MAAC,UAAU,EAAE,SAAS,YAAY,SAAS,OAAO,MAAM;AAC1D,YAAI,CAAC,EAAE;AAAS;AAChB,cAAMC,gBAAI,6BAA6B,EAAE,EAAE;AAC3C,aAAK,OAAO;AAAA,MACb,EAAC,CAAC;AAAA,IACH;AAAA,EACD;AACD;;;;;;;;;;;;;;;;;;;;;;AClDA,GAAG,WAAW,eAAe;"}
|
||||
1
frontend/unpackage/dist/dev/.sourcemap/mp-weixin/pages/product/edit.js.map
vendored
Normal file
1
frontend/unpackage/dist/dev/.sourcemap/mp-weixin/pages/product/edit.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
1
frontend/unpackage/dist/dev/.sourcemap/mp-weixin/pages/product/form.js.map
vendored
Normal file
1
frontend/unpackage/dist/dev/.sourcemap/mp-weixin/pages/product/form.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
1
frontend/unpackage/dist/dev/.sourcemap/mp-weixin/pages/product/list.js.map
vendored
Normal file
1
frontend/unpackage/dist/dev/.sourcemap/mp-weixin/pages/product/list.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
||||
{"version":3,"file":"select.js","sources":["pages/product/select.vue","../../../../Downloads/HBuilderX.4.76.2025082103/HBuilderX/plugins/uniapp-cli-vite/uniPage:/cGFnZXMvcHJvZHVjdC9zZWxlY3QudnVl"],"sourcesContent":["<template>\r\n\t<view class=\"page\">\r\n\t\t<view class=\"search\">\r\n\t\t\t<input v-model=\"kw\" placeholder=\"搜索商品名称/编码\" @confirm=\"search\" />\r\n\t\t\t<button size=\"mini\" @click=\"search\">搜索</button>\r\n\t\t</view>\r\n\t\t<scroll-view scroll-y class=\"list\">\r\n\t\t\t<view class=\"item\" v-for=\"p in products\" :key=\"p.id\" @click=\"select(p)\">\r\n\t\t\t\t<view class=\"name\">{{ p.name }}</view>\r\n\t\t\t\t<view class=\"meta\">{{ p.code }} · 库存:{{ p.stock || 0 }}</view>\r\n\t\t\t</view>\r\n\t\t</scroll-view>\r\n\t</view>\r\n</template>\r\n\r\n<script>\r\n\timport { get } from '../../common/http.js'\r\n\texport default {\r\n\t\tdata() { return { kw: '', products: [] } },\r\n\t\tonLoad() { this.search() },\r\n\t\tmethods: {\r\n\t\t\tasync search() {\r\n\t\t\t\ttry {\r\n\t\t\t\t\tconst res = await get('/api/products', { kw: this.kw, page: 1, size: 50 })\r\n\t\t\t\t\tthis.products = Array.isArray(res?.list) ? res.list : (Array.isArray(res) ? res : [])\r\n\t\t\t\t} catch(e) { uni.showToast({ title: '加载失败', icon: 'none' }) }\r\n\t\t\t},\r\n\t\t\tselect(p) {\r\n\t\t\t\tconst opener = getCurrentPages()[getCurrentPages().length-2]\r\n\t\t\t\tif (opener && opener.$vm && opener.$vm.items) {\r\n\t\t\t\t\topener.$vm.items.push({ productId: p.id, productName: p.name, quantity: 1, unitPrice: Number(p.price || 0) })\r\n\t\t\t\t}\r\n\t\t\t\tuni.navigateBack()\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n</script>\r\n\r\n<style>\r\n\t.page { display:flex; flex-direction: column; height: 100vh; }\r\n\t.search { display:flex; gap: 12rpx; padding: 16rpx; background:#fff; }\r\n\t.search input { flex:1; background:#f6f6f6; border-radius: 12rpx; padding: 12rpx; }\r\n\t.list { flex:1; }\r\n\t.item { padding: 20rpx 24rpx; background:#fff; border-bottom: 1rpx solid #f1f1f1; }\r\n\t.name { color:#333; margin-bottom: 6rpx; }\r\n\t.meta { color:#888; font-size: 24rpx; }\r\n</style>\r\n\r\n\r\n","import MiniProgramPage from 'C:/Users/21826/Desktop/Wj/PartsInquiry/frontend/pages/product/select.vue'\nwx.createPage(MiniProgramPage)"],"names":["get","uni"],"mappings":";;;AAiBC,MAAK,YAAU;AAAA,EACd,OAAO;AAAE,WAAO,EAAE,IAAI,IAAI,UAAU,CAAA;EAAM;AAAA,EAC1C,SAAS;AAAE,SAAK;EAAU;AAAA,EAC1B,SAAS;AAAA,IACR,MAAM,SAAS;AACd,UAAI;AACH,cAAM,MAAM,MAAMA,gBAAI,iBAAiB,EAAE,IAAI,KAAK,IAAI,MAAM,GAAG,MAAM,GAAC,CAAG;AACzE,aAAK,WAAW,MAAM,QAAQ,2BAAK,IAAI,IAAI,IAAI,OAAQ,MAAM,QAAQ,GAAG,IAAI,MAAM,CAAA;AAAA,eAC3E,GAAG;AAAEC,sBAAAA,MAAI,UAAU,EAAE,OAAO,QAAQ,MAAM,OAAO,CAAC;AAAA,MAAE;AAAA,IAC5D;AAAA,IACD,OAAO,GAAG;AACT,YAAM,SAAS,gBAAiB,EAAC,gBAAe,EAAG,SAAO,CAAC;AAC3D,UAAI,UAAU,OAAO,OAAO,OAAO,IAAI,OAAO;AAC7C,eAAO,IAAI,MAAM,KAAK,EAAE,WAAW,EAAE,IAAI,aAAa,EAAE,MAAM,UAAU,GAAG,WAAW,OAAO,EAAE,SAAS,CAAC,GAAG;AAAA,MAC7G;AACAA,oBAAAA,MAAI,aAAa;AAAA,IAClB;AAAA,EACD;AACD;;;;;;;;;;;;;;;;;;;AClCD,GAAG,WAAW,eAAe;"}
|
||||
{"version":3,"file":"select.js","sources":["pages/product/select.vue","../../../../Downloads/HBuilderX.4.76.2025082103/HBuilderX/plugins/uniapp-cli-vite/uniPage:/cGFnZXMvcHJvZHVjdC9zZWxlY3QudnVl"],"sourcesContent":["<template>\r\n\t<view class=\"page\">\r\n\t\t<view class=\"search\">\r\n\t\t\t<input v-model=\"kw\" placeholder=\"搜索商品名称/编码\" @confirm=\"search\" />\r\n\t\t\t<button size=\"mini\" @click=\"search\">搜索</button>\r\n\t\t</view>\r\n\t\t<scroll-view scroll-y class=\"list\">\r\n\t\t\t<view class=\"item\" v-for=\"p in products\" :key=\"p.id\" @click=\"select(p)\">\r\n\t\t\t\t<view class=\"name\">{{ p.name }}</view>\r\n\t\t\t\t<view class=\"meta\">{{ p.code }} · 库存:{{ p.stock || 0 }}</view>\r\n\t\t\t</view>\r\n\t\t</scroll-view>\r\n\t</view>\r\n</template>\r\n\r\n<script>\r\n\timport { get } from '../../common/http.js'\r\n\texport default {\r\n\t\tdata() { return { kw: '', products: [] } },\r\n\t\tonLoad() { this.search() },\r\n\t\tmethods: {\r\n\t\t\tasync search() {\r\n\t\t\t\ttry {\r\n\t\t\t\t\tconst res = await get('/api/products', { kw: this.kw, page: 1, size: 50 })\r\n\t\t\t\t\tthis.products = Array.isArray(res?.list) ? res.list : (Array.isArray(res) ? res : [])\r\n\t\t\t\t} catch(e) { uni.showToast({ title: '加载失败', icon: 'none' }) }\r\n\t\t\t},\r\n\t\t\tselect(p) {\r\n\t\t\t\tconst opener = getCurrentPages()[getCurrentPages().length-2]\r\n\t\t\t\tif (opener && opener.$vm && opener.$vm.items) {\r\n\t\t\t\t\topener.$vm.items.push({ productId: p.id, productName: p.name, quantity: 1, unitPrice: Number(p.price || 0) })\r\n\t\t\t\t}\r\n\t\t\t\tuni.navigateBack()\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n</script>\r\n\r\n<style>\r\n\t.page { display:flex; flex-direction: column; height: 100vh; }\r\n\t.search { display:flex; gap: 12rpx; padding: 16rpx; background:#fff; }\r\n\t.search input { flex:1; background:#f6f6f6; border-radius: 12rpx; padding: 12rpx; }\r\n\t.list { flex:1; }\r\n\t.item { padding: 20rpx 24rpx; background:#fff; border-bottom: 1rpx solid #f1f1f1; }\r\n\t.name { color:#333; margin-bottom: 6rpx; }\r\n\t.meta { color:#888; font-size: 24rpx; }\r\n</style>\r\n\r\n\r\n\r\n","import MiniProgramPage from 'C:/Users/21826/Desktop/Wj/PartsInquiry/frontend/pages/product/select.vue'\nwx.createPage(MiniProgramPage)"],"names":["get","uni"],"mappings":";;;AAiBC,MAAK,YAAU;AAAA,EACd,OAAO;AAAE,WAAO,EAAE,IAAI,IAAI,UAAU,CAAA;EAAM;AAAA,EAC1C,SAAS;AAAE,SAAK;EAAU;AAAA,EAC1B,SAAS;AAAA,IACR,MAAM,SAAS;AACd,UAAI;AACH,cAAM,MAAM,MAAMA,gBAAI,iBAAiB,EAAE,IAAI,KAAK,IAAI,MAAM,GAAG,MAAM,GAAC,CAAG;AACzE,aAAK,WAAW,MAAM,QAAQ,2BAAK,IAAI,IAAI,IAAI,OAAQ,MAAM,QAAQ,GAAG,IAAI,MAAM,CAAA;AAAA,eAC3E,GAAG;AAAEC,sBAAAA,MAAI,UAAU,EAAE,OAAO,QAAQ,MAAM,OAAO,CAAC;AAAA,MAAE;AAAA,IAC5D;AAAA,IACD,OAAO,GAAG;AACT,YAAM,SAAS,gBAAiB,EAAC,gBAAe,EAAG,SAAO,CAAC;AAC3D,UAAI,UAAU,OAAO,OAAO,OAAO,IAAI,OAAO;AAC7C,eAAO,IAAI,MAAM,KAAK,EAAE,WAAW,EAAE,IAAI,aAAa,EAAE,MAAM,UAAU,GAAG,WAAW,OAAO,EAAE,SAAS,CAAC,GAAG;AAAA,MAC7G;AACAA,oBAAAA,MAAI,aAAa;AAAA,IAClB;AAAA,EACD;AACD;;;;;;;;;;;;;;;;;;;AClCD,GAAG,WAAW,eAAe;"}
|
||||
1
frontend/unpackage/dist/dev/.sourcemap/mp-weixin/pages/product/settings.js.map
vendored
Normal file
1
frontend/unpackage/dist/dev/.sourcemap/mp-weixin/pages/product/settings.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"settings.js","sources":["pages/product/settings.vue","../../../../Downloads/HBuilderX.4.76.2025082103/HBuilderX/plugins/uniapp-cli-vite/uniPage:/cGFnZXMvcHJvZHVjdC9zZXR0aW5ncy52dWU"],"sourcesContent":["<template>\r\n\t<view class=\"page\">\r\n\t\t<view class=\"item\">\r\n\t\t\t<text>隐藏零库存商品</text>\r\n\t\t\t<switch :checked=\"settings.hideZeroStock\" @change=\"(e)=>update('hideZeroStock', e.detail.value)\" />\r\n\t\t</view>\r\n\t\t<view class=\"item\">\r\n\t\t\t<text>隐藏进货价</text>\r\n\t\t\t<switch :checked=\"settings.hidePurchasePrice\" @change=\"(e)=>update('hidePurchasePrice', e.detail.value)\" />\r\n\t\t</view>\r\n\t</view>\r\n</template>\r\n\r\n<script>\r\nimport { get, put } from '../../common/http.js'\r\n\r\nexport default {\r\n\tdata() {\r\n\t\treturn { settings: { hideZeroStock: false, hidePurchasePrice: false } }\r\n\t},\r\n\tonLoad() { this.load() },\r\n\tmethods: {\r\n\t\tasync load() {\r\n\t\t\ttry {\r\n\t\t\t\tconst res = await get('/api/product-settings')\r\n\t\t\t\tthis.settings = { hideZeroStock: !!res?.hideZeroStock, hidePurchasePrice: !!res?.hidePurchasePrice }\r\n\t\t\t} catch (_) {}\r\n\t\t},\r\n\t\tasync update(key, val) {\r\n\t\t\tconst next = { ...this.settings, [key]: val }\r\n\t\t\tthis.settings = next\r\n\t\t\ttry { await put('/api/product-settings', next) } catch (_) {}\r\n\t\t}\r\n\t}\r\n}\r\n</script>\r\n\r\n<style>\r\n.page { background:#fff; }\r\n.item { display:flex; justify-content: space-between; align-items:center; padding: 20rpx; border-bottom: 1rpx solid #f1f1f1; }\r\n</style>\r\n\r\n\r\n\r\n\r\n","import MiniProgramPage from 'C:/Users/21826/Desktop/Wj/PartsInquiry/frontend/pages/product/settings.vue'\nwx.createPage(MiniProgramPage)"],"names":["get","put"],"mappings":";;;AAgBA,MAAK,YAAU;AAAA,EACd,OAAO;AACN,WAAO,EAAE,UAAU,EAAE,eAAe,OAAO,mBAAmB,QAAQ;AAAA,EACtE;AAAA,EACD,SAAS;AAAE,SAAK;EAAQ;AAAA,EACxB,SAAS;AAAA,IACR,MAAM,OAAO;AACZ,UAAI;AACH,cAAM,MAAM,MAAMA,YAAG,IAAC,uBAAuB;AAC7C,aAAK,WAAW,EAAE,eAAe,CAAC,EAAC,2BAAK,gBAAe,mBAAmB,CAAC,EAAC,2BAAK,mBAAkB;AAAA,eAC3F,GAAG;AAAA,MAAC;AAAA,IACb;AAAA,IACD,MAAM,OAAO,KAAK,KAAK;AACtB,YAAM,OAAO,EAAE,GAAG,KAAK,UAAU,CAAC,GAAG,GAAG,IAAI;AAC5C,WAAK,WAAW;AAChB,UAAI;AAAE,cAAMC,YAAAA,IAAI,yBAAyB,IAAI;AAAA,MAAI,SAAO,GAAG;AAAA,MAAC;AAAA,IAC7D;AAAA,EACD;AACD;;;;;;;;;;ACjCA,GAAG,WAAW,eAAe;"}
|
||||
1
frontend/unpackage/dist/dev/.sourcemap/mp-weixin/pages/product/units.js.map
vendored
Normal file
1
frontend/unpackage/dist/dev/.sourcemap/mp-weixin/pages/product/units.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"units.js","sources":["pages/product/units.vue","../../../../Downloads/HBuilderX.4.76.2025082103/HBuilderX/plugins/uniapp-cli-vite/uniPage:/cGFnZXMvcHJvZHVjdC91bml0cy52dWU"],"sourcesContent":["<template>\r\n\t<view class=\"page\">\r\n\t\t<view class=\"toolbar\">\r\n\t\t\t<input v-model.trim=\"name\" placeholder=\"新单位名称\" />\r\n\t\t\t<button size=\"mini\" @click=\"create\">新增</button>\r\n\t\t</view>\r\n\t\t<scroll-view scroll-y class=\"list\">\r\n\t\t\t<view class=\"item\" v-for=\"u in list\" :key=\"u.id\">\r\n\t\t\t\t<input v-model.trim=\"u.name\" />\r\n\t\t\t\t<view class=\"ops\">\r\n\t\t\t\t\t<button size=\"mini\" @click=\"update(u)\">保存</button>\r\n\t\t\t\t\t<button size=\"mini\" type=\"warn\" @click=\"remove(u)\">删除</button>\r\n\t\t\t\t</view>\r\n\t\t\t</view>\r\n\t\t</scroll-view>\r\n\t</view>\r\n</template>\r\n\r\n<script>\r\nimport { get, post, put, del } from '../../common/http.js'\r\n\r\nexport default {\r\n\tdata() {\r\n\t\treturn { name: '', list: [] }\r\n\t},\r\n\tonLoad() { this.reload() },\r\n\tmethods: {\r\n\t\tasync reload() {\r\n\t\t\ttry {\r\n\t\t\t\tconst res = await get('/api/product-units')\r\n\t\t\t\tthis.list = Array.isArray(res?.list) ? res.list : (Array.isArray(res) ? res : [])\r\n\t\t\t} catch (_) {}\r\n\t\t},\r\n\t\tasync create() {\r\n\t\t\tif (!this.name) return\r\n\t\t\tawait post('/api/product-units', { name: this.name })\r\n\t\t\tthis.name = ''\r\n\t\t\tthis.reload()\r\n\t\t},\r\n\t\tasync update(u) {\r\n\t\t\tawait put('/api/product-units/' + u.id, { name: u.name })\r\n\t\t\tuni.showToast({ title: '已保存', icon: 'success' })\r\n\t\t},\r\n\t\tasync remove(u) {\r\n\t\t\tuni.showModal({ content: '确定删除该单位?', success: async (r) => {\r\n\t\t\t\tif (!r.confirm) return\r\n\t\t\t\tawait del('/api/product-units/' + u.id)\r\n\t\t\t\tthis.reload()\r\n\t\t\t}})\r\n\t\t}\r\n\t}\r\n}\r\n</script>\r\n\r\n<style>\r\n.page { display:flex; flex-direction: column; height: 100vh; }\r\n.toolbar { display:flex; gap: 12rpx; padding: 16rpx; background:#fff; }\r\n.toolbar input { flex:1; background:#f6f6f6; border-radius: 12rpx; padding: 12rpx; }\r\n.list { flex:1; }\r\n.item { display:flex; gap: 12rpx; align-items:center; padding: 16rpx; background:#fff; border-bottom: 1rpx solid #f1f1f1; }\r\n.item input { flex:1; background:#f7f7f7; border-radius: 10rpx; padding: 12rpx; }\r\n.ops { display:flex; gap: 10rpx; }\r\n</style>\r\n\r\n\r\n\r\n\r\n","import MiniProgramPage from 'C:/Users/21826/Desktop/Wj/PartsInquiry/frontend/pages/product/units.vue'\nwx.createPage(MiniProgramPage)"],"names":["get","post","put","uni","del"],"mappings":";;;AAqBA,MAAK,YAAU;AAAA,EACd,OAAO;AACN,WAAO,EAAE,MAAM,IAAI,MAAM,CAAA,EAAG;AAAA,EAC5B;AAAA,EACD,SAAS;AAAE,SAAK;EAAU;AAAA,EAC1B,SAAS;AAAA,IACR,MAAM,SAAS;AACd,UAAI;AACH,cAAM,MAAM,MAAMA,YAAG,IAAC,oBAAoB;AAC1C,aAAK,OAAO,MAAM,QAAQ,2BAAK,IAAI,IAAI,IAAI,OAAQ,MAAM,QAAQ,GAAG,IAAI,MAAM,CAAA;AAAA,eACtE,GAAG;AAAA,MAAC;AAAA,IACb;AAAA,IACD,MAAM,SAAS;AACd,UAAI,CAAC,KAAK;AAAM;AAChB,YAAMC,YAAAA,KAAK,sBAAsB,EAAE,MAAM,KAAK,KAAG,CAAG;AACpD,WAAK,OAAO;AACZ,WAAK,OAAO;AAAA,IACZ;AAAA,IACD,MAAM,OAAO,GAAG;AACf,YAAMC,YAAG,IAAC,wBAAwB,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM;AACxDC,oBAAG,MAAC,UAAU,EAAE,OAAO,OAAO,MAAM,WAAW;AAAA,IAC/C;AAAA,IACD,MAAM,OAAO,GAAG;AACfA,oBAAG,MAAC,UAAU,EAAE,SAAS,YAAY,SAAS,OAAO,MAAM;AAC1D,YAAI,CAAC,EAAE;AAAS;AAChB,cAAMC,gBAAI,wBAAwB,EAAE,EAAE;AACtC,aAAK,OAAO;AAAA,MACb,EAAC,CAAC;AAAA,IACH;AAAA,EACD;AACD;;;;;;;;;;;;;;;;;;;;;;AClDA,GAAG,WAAW,eAAe;"}
|
||||
@@ -1 +1 @@
|
||||
{"version":3,"file":"select.js","sources":["pages/supplier/select.vue","../../../../Downloads/HBuilderX.4.76.2025082103/HBuilderX/plugins/uniapp-cli-vite/uniPage:/cGFnZXMvc3VwcGxpZXIvc2VsZWN0LnZ1ZQ"],"sourcesContent":["<template>\r\n\t<view class=\"page\">\r\n\t\t<view class=\"search\">\r\n\t\t\t<input v-model=\"kw\" placeholder=\"搜索供应商名称/电话\" @confirm=\"search\" />\r\n\t\t\t<button size=\"mini\" @click=\"search\">搜索</button>\r\n\t\t</view>\r\n\t\t<scroll-view scroll-y class=\"list\">\r\n\t\t\t<view class=\"item\" v-for=\"s in suppliers\" :key=\"s.id\" @click=\"select(s)\">\r\n\t\t\t\t<view class=\"name\">{{ s.name }}</view>\r\n\t\t\t\t<view class=\"meta\">{{ s.mobile || '—' }}</view>\r\n\t\t\t</view>\r\n\t\t</scroll-view>\r\n\t</view>\r\n</template>\r\n\r\n<script>\r\n\timport { get } from '../../common/http.js'\r\n\texport default {\r\n\t\tdata() { return { kw: '', suppliers: [] } },\r\n\t\tonLoad() { this.search() },\r\n\t\tmethods: {\r\n\t\t\tasync search() {\r\n\t\t\t\ttry {\r\n\t\t\t\t\tconst res = await get('/api/suppliers', { kw: this.kw, page: 1, size: 50 })\r\n\t\t\t\t\tthis.suppliers = Array.isArray(res?.list) ? res.list : (Array.isArray(res) ? res : [])\r\n\t\t\t\t} catch(e) { uni.showToast({ title: '加载失败', icon: 'none' }) }\r\n\t\t\t},\r\n\t\t\tselect(s) {\r\n\t\t\t\tconst opener = getCurrentPages()[getCurrentPages().length-2]\r\n\t\t\t\tif (opener && opener.$vm) {\r\n\t\t\t\t\topener.$vm.order.supplierId = s.id\r\n\t\t\t\t\topener.$vm.supplierName = s.name\r\n\t\t\t\t}\r\n\t\t\t\tuni.navigateBack()\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n</script>\r\n\r\n<style>\r\n\t.page { display:flex; flex-direction: column; height: 100vh; }\r\n\t.search { display:flex; gap: 12rpx; padding: 16rpx; background:#fff; }\r\n\t.search input { flex:1; background:#f6f6f6; border-radius: 12rpx; padding: 12rpx; }\r\n\t.list { flex:1; }\r\n\t.item { padding: 20rpx 24rpx; background:#fff; border-bottom: 1rpx solid #f1f1f1; }\r\n\t.name { color:#333; margin-bottom: 6rpx; }\r\n\t.meta { color:#888; font-size: 24rpx; }\r\n</style>\r\n\r\n\r\n","import MiniProgramPage from 'C:/Users/21826/Desktop/Wj/PartsInquiry/frontend/pages/supplier/select.vue'\nwx.createPage(MiniProgramPage)"],"names":["get","uni"],"mappings":";;;AAiBC,MAAK,YAAU;AAAA,EACd,OAAO;AAAE,WAAO,EAAE,IAAI,IAAI,WAAW,CAAA;EAAM;AAAA,EAC3C,SAAS;AAAE,SAAK;EAAU;AAAA,EAC1B,SAAS;AAAA,IACR,MAAM,SAAS;AACd,UAAI;AACH,cAAM,MAAM,MAAMA,gBAAI,kBAAkB,EAAE,IAAI,KAAK,IAAI,MAAM,GAAG,MAAM,GAAC,CAAG;AAC1E,aAAK,YAAY,MAAM,QAAQ,2BAAK,IAAI,IAAI,IAAI,OAAQ,MAAM,QAAQ,GAAG,IAAI,MAAM,CAAA;AAAA,eAC5E,GAAG;AAAEC,sBAAAA,MAAI,UAAU,EAAE,OAAO,QAAQ,MAAM,OAAO,CAAC;AAAA,MAAE;AAAA,IAC5D;AAAA,IACD,OAAO,GAAG;AACT,YAAM,SAAS,gBAAiB,EAAC,gBAAe,EAAG,SAAO,CAAC;AAC3D,UAAI,UAAU,OAAO,KAAK;AACzB,eAAO,IAAI,MAAM,aAAa,EAAE;AAChC,eAAO,IAAI,eAAe,EAAE;AAAA,MAC7B;AACAA,oBAAAA,MAAI,aAAa;AAAA,IAClB;AAAA,EACD;AACD;;;;;;;;;;;;;;;;;;ACnCD,GAAG,WAAW,eAAe;"}
|
||||
{"version":3,"file":"select.js","sources":["pages/supplier/select.vue","../../../../Downloads/HBuilderX.4.76.2025082103/HBuilderX/plugins/uniapp-cli-vite/uniPage:/cGFnZXMvc3VwcGxpZXIvc2VsZWN0LnZ1ZQ"],"sourcesContent":["<template>\r\n\t<view class=\"page\">\r\n\t\t<view class=\"search\">\r\n\t\t\t<input v-model=\"kw\" placeholder=\"搜索供应商名称/电话\" @confirm=\"search\" />\r\n\t\t\t<button size=\"mini\" @click=\"search\">搜索</button>\r\n\t\t</view>\r\n\t\t<scroll-view scroll-y class=\"list\">\r\n\t\t\t<view class=\"item\" v-for=\"s in suppliers\" :key=\"s.id\" @click=\"select(s)\">\r\n\t\t\t\t<view class=\"name\">{{ s.name }}</view>\r\n\t\t\t\t<view class=\"meta\">{{ s.mobile || '—' }}</view>\r\n\t\t\t</view>\r\n\t\t</scroll-view>\r\n\t</view>\r\n</template>\r\n\r\n<script>\r\n\timport { get } from '../../common/http.js'\r\n\texport default {\r\n\t\tdata() { return { kw: '', suppliers: [] } },\r\n\t\tonLoad() { this.search() },\r\n\t\tmethods: {\r\n\t\t\tasync search() {\r\n\t\t\t\ttry {\r\n\t\t\t\t\tconst res = await get('/api/suppliers', { kw: this.kw, page: 1, size: 50 })\r\n\t\t\t\t\tthis.suppliers = Array.isArray(res?.list) ? res.list : (Array.isArray(res) ? res : [])\r\n\t\t\t\t} catch(e) { uni.showToast({ title: '加载失败', icon: 'none' }) }\r\n\t\t\t},\r\n\t\t\tselect(s) {\r\n\t\t\t\tconst opener = getCurrentPages()[getCurrentPages().length-2]\r\n\t\t\t\tif (opener && opener.$vm) {\r\n\t\t\t\t\topener.$vm.order.supplierId = s.id\r\n\t\t\t\t\topener.$vm.supplierName = s.name\r\n\t\t\t\t}\r\n\t\t\t\tuni.navigateBack()\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n</script>\r\n\r\n<style>\r\n\t.page { display:flex; flex-direction: column; height: 100vh; }\r\n\t.search { display:flex; gap: 12rpx; padding: 16rpx; background:#fff; }\r\n\t.search input { flex:1; background:#f6f6f6; border-radius: 12rpx; padding: 12rpx; }\r\n\t.list { flex:1; }\r\n\t.item { padding: 20rpx 24rpx; background:#fff; border-bottom: 1rpx solid #f1f1f1; }\r\n\t.name { color:#333; margin-bottom: 6rpx; }\r\n\t.meta { color:#888; font-size: 24rpx; }\r\n</style>\r\n\r\n\r\n\r\n","import MiniProgramPage from 'C:/Users/21826/Desktop/Wj/PartsInquiry/frontend/pages/supplier/select.vue'\nwx.createPage(MiniProgramPage)"],"names":["get","uni"],"mappings":";;;AAiBC,MAAK,YAAU;AAAA,EACd,OAAO;AAAE,WAAO,EAAE,IAAI,IAAI,WAAW,CAAA;EAAM;AAAA,EAC3C,SAAS;AAAE,SAAK;EAAU;AAAA,EAC1B,SAAS;AAAA,IACR,MAAM,SAAS;AACd,UAAI;AACH,cAAM,MAAM,MAAMA,gBAAI,kBAAkB,EAAE,IAAI,KAAK,IAAI,MAAM,GAAG,MAAM,GAAC,CAAG;AAC1E,aAAK,YAAY,MAAM,QAAQ,2BAAK,IAAI,IAAI,IAAI,OAAQ,MAAM,QAAQ,GAAG,IAAI,MAAM,CAAA;AAAA,eAC5E,GAAG;AAAEC,sBAAAA,MAAI,UAAU,EAAE,OAAO,QAAQ,MAAM,OAAO,CAAC;AAAA,MAAE;AAAA,IAC5D;AAAA,IACD,OAAO,GAAG;AACT,YAAM,SAAS,gBAAiB,EAAC,gBAAe,EAAG,SAAO,CAAC;AAC3D,UAAI,UAAU,OAAO,KAAK;AACzB,eAAO,IAAI,MAAM,aAAa,EAAE;AAChC,eAAO,IAAI,eAAe,EAAE;AAAA,MAC7B;AACAA,oBAAAA,MAAI,aAAa;AAAA,IAClB;AAAA,EACD;AACD;;;;;;;;;;;;;;;;;;ACnCD,GAAG,WAAW,eAAe;"}
|
||||
5
frontend/unpackage/dist/dev/mp-weixin/app.js
vendored
5
frontend/unpackage/dist/dev/mp-weixin/app.js
vendored
@@ -5,6 +5,11 @@ if (!Math) {
|
||||
"./pages/index/index.js";
|
||||
"./pages/order/create.js";
|
||||
"./pages/product/select.js";
|
||||
"./pages/product/list.js";
|
||||
"./pages/product/form.js";
|
||||
"./pages/product/categories.js";
|
||||
"./pages/product/units.js";
|
||||
"./pages/product/settings.js";
|
||||
"./pages/customer/select.js";
|
||||
"./pages/supplier/select.js";
|
||||
"./pages/account/select.js";
|
||||
|
||||
@@ -3,6 +3,11 @@
|
||||
"pages/index/index",
|
||||
"pages/order/create",
|
||||
"pages/product/select",
|
||||
"pages/product/list",
|
||||
"pages/product/form",
|
||||
"pages/product/categories",
|
||||
"pages/product/units",
|
||||
"pages/product/settings",
|
||||
"pages/customer/select",
|
||||
"pages/supplier/select",
|
||||
"pages/account/select"
|
||||
|
||||
@@ -2,18 +2,21 @@
|
||||
const common_vendor = require("./vendor.js");
|
||||
const envBaseUrl = typeof process !== "undefined" && process.env && (process.env.VITE_APP_API_BASE_URL || process.env.API_BASE_URL) || "";
|
||||
const storageBaseUrl = typeof common_vendor.index !== "undefined" ? common_vendor.index.getStorageSync("API_BASE_URL") || "" : "";
|
||||
const fallbackBaseUrl = "http://localhost:8080";
|
||||
const fallbackBaseUrl = "http://192.168.31.193:8080";
|
||||
const API_BASE_URL = (envBaseUrl || storageBaseUrl || fallbackBaseUrl).replace(/\/$/, "");
|
||||
const candidateBases = [envBaseUrl, storageBaseUrl, fallbackBaseUrl, "http://127.0.0.1:8080", "http://localhost:8080"];
|
||||
const API_BASE_URL_CANDIDATES = Array.from(new Set(candidateBases.filter(Boolean))).map((u) => String(u).replace(/\/$/, ""));
|
||||
const envShopId = typeof process !== "undefined" && process.env && (process.env.VITE_APP_SHOP_ID || process.env.SHOP_ID) || "";
|
||||
const storageShopId = typeof common_vendor.index !== "undefined" ? common_vendor.index.getStorageSync("SHOP_ID") || "" : "";
|
||||
const SHOP_ID = Number(envShopId || storageShopId || 1);
|
||||
const envEnableDefaultUser = typeof process !== "undefined" && process.env && (process.env.VITE_APP_ENABLE_DEFAULT_USER || process.env.ENABLE_DEFAULT_USER) || "";
|
||||
const storageEnableDefaultUser = typeof common_vendor.index !== "undefined" ? common_vendor.index.getStorageSync("ENABLE_DEFAULT_USER") || "" : "";
|
||||
const ENABLE_DEFAULT_USER = String(envEnableDefaultUser || storageEnableDefaultUser || "false").toLowerCase() === "true";
|
||||
const ENABLE_DEFAULT_USER = String(envEnableDefaultUser || storageEnableDefaultUser || "true").toLowerCase() === "true";
|
||||
const envDefaultUserId = typeof process !== "undefined" && process.env && (process.env.VITE_APP_DEFAULT_USER_ID || process.env.DEFAULT_USER_ID) || "";
|
||||
const storageDefaultUserId = typeof common_vendor.index !== "undefined" ? common_vendor.index.getStorageSync("DEFAULT_USER_ID") || "" : "";
|
||||
const DEFAULT_USER_ID = Number(envDefaultUserId || storageDefaultUserId || 2);
|
||||
exports.API_BASE_URL = API_BASE_URL;
|
||||
exports.API_BASE_URL_CANDIDATES = API_BASE_URL_CANDIDATES;
|
||||
exports.DEFAULT_USER_ID = DEFAULT_USER_ID;
|
||||
exports.ENABLE_DEFAULT_USER = ENABLE_DEFAULT_USER;
|
||||
exports.SHOP_ID = SHOP_ID;
|
||||
|
||||
103
frontend/unpackage/dist/dev/mp-weixin/common/http.js
vendored
103
frontend/unpackage/dist/dev/mp-weixin/common/http.js
vendored
@@ -8,24 +8,29 @@ function buildUrl(path) {
|
||||
return path;
|
||||
return common_config.API_BASE_URL + (path.startsWith("/") ? path : "/" + path);
|
||||
}
|
||||
function requestWithFallback(options, candidates, idx, resolve, reject) {
|
||||
const base = candidates[idx] || common_config.API_BASE_URL;
|
||||
const url = options.url.replace(/^https?:\/\/[^/]+/, base);
|
||||
common_vendor.index.request({ ...options, url, success: (res) => {
|
||||
const { statusCode, data } = res;
|
||||
if (statusCode >= 200 && statusCode < 300)
|
||||
return resolve(data);
|
||||
if (idx + 1 < candidates.length)
|
||||
return requestWithFallback(options, candidates, idx + 1, resolve, reject);
|
||||
reject(new Error("HTTP " + statusCode));
|
||||
}, fail: (err) => {
|
||||
if (idx + 1 < candidates.length)
|
||||
return requestWithFallback(options, candidates, idx + 1, resolve, reject);
|
||||
reject(err);
|
||||
} });
|
||||
}
|
||||
function get(path, params = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const headers = { "X-Shop-Id": common_config.SHOP_ID };
|
||||
if (common_config.ENABLE_DEFAULT_USER && common_config.DEFAULT_USER_ID)
|
||||
headers["X-User-Id"] = common_config.DEFAULT_USER_ID;
|
||||
common_vendor.index.request({
|
||||
url: buildUrl(path),
|
||||
method: "GET",
|
||||
data: params,
|
||||
header: headers,
|
||||
success: (res) => {
|
||||
const { statusCode, data } = res;
|
||||
if (statusCode >= 200 && statusCode < 300)
|
||||
return resolve(data);
|
||||
reject(new Error("HTTP " + statusCode));
|
||||
},
|
||||
fail: (err) => reject(err)
|
||||
});
|
||||
const options = { url: buildUrl(path), method: "GET", data: params, header: headers };
|
||||
requestWithFallback(options, common_config.API_BASE_URL_CANDIDATES, 0, resolve, reject);
|
||||
});
|
||||
}
|
||||
function post(path, body = {}) {
|
||||
@@ -33,21 +38,67 @@ function post(path, body = {}) {
|
||||
const headers = { "Content-Type": "application/json", "X-Shop-Id": common_config.SHOP_ID };
|
||||
if (common_config.ENABLE_DEFAULT_USER && common_config.DEFAULT_USER_ID)
|
||||
headers["X-User-Id"] = common_config.DEFAULT_USER_ID;
|
||||
common_vendor.index.request({
|
||||
url: buildUrl(path),
|
||||
method: "POST",
|
||||
data: body,
|
||||
header: headers,
|
||||
success: (res) => {
|
||||
const { statusCode, data } = res;
|
||||
if (statusCode >= 200 && statusCode < 300)
|
||||
return resolve(data);
|
||||
reject(new Error("HTTP " + statusCode));
|
||||
},
|
||||
fail: (err) => reject(err)
|
||||
});
|
||||
const options = { url: buildUrl(path), method: "POST", data: body, header: headers };
|
||||
requestWithFallback(options, common_config.API_BASE_URL_CANDIDATES, 0, resolve, reject);
|
||||
});
|
||||
}
|
||||
function put(path, body = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const headers = { "Content-Type": "application/json", "X-Shop-Id": common_config.SHOP_ID };
|
||||
if (common_config.ENABLE_DEFAULT_USER && common_config.DEFAULT_USER_ID)
|
||||
headers["X-User-Id"] = common_config.DEFAULT_USER_ID;
|
||||
const options = { url: buildUrl(path), method: "PUT", data: body, header: headers };
|
||||
requestWithFallback(options, common_config.API_BASE_URL_CANDIDATES, 0, resolve, reject);
|
||||
});
|
||||
}
|
||||
function del(path, body = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const headers = { "Content-Type": "application/json", "X-Shop-Id": common_config.SHOP_ID };
|
||||
if (common_config.ENABLE_DEFAULT_USER && common_config.DEFAULT_USER_ID)
|
||||
headers["X-User-Id"] = common_config.DEFAULT_USER_ID;
|
||||
const options = { url: buildUrl(path), method: "DELETE", data: body, header: headers };
|
||||
requestWithFallback(options, common_config.API_BASE_URL_CANDIDATES, 0, resolve, reject);
|
||||
});
|
||||
}
|
||||
function uploadWithFallback(options, candidates, idx, resolve, reject) {
|
||||
const base = candidates[idx] || common_config.API_BASE_URL;
|
||||
const url = options.url.replace(/^https?:\/\/[^/]+/, base);
|
||||
const uploadOptions = { ...options, url };
|
||||
common_vendor.index.uploadFile({
|
||||
...uploadOptions,
|
||||
success: (res) => {
|
||||
const statusCode = res.statusCode || 0;
|
||||
if (statusCode >= 200 && statusCode < 300) {
|
||||
try {
|
||||
const data = typeof res.data === "string" ? JSON.parse(res.data) : res.data;
|
||||
return resolve(data);
|
||||
} catch (e) {
|
||||
return resolve(res.data);
|
||||
}
|
||||
}
|
||||
if (idx + 1 < candidates.length)
|
||||
return uploadWithFallback(options, candidates, idx + 1, resolve, reject);
|
||||
reject(new Error("HTTP " + statusCode));
|
||||
},
|
||||
fail: (err) => {
|
||||
if (idx + 1 < candidates.length)
|
||||
return uploadWithFallback(options, candidates, idx + 1, resolve, reject);
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
}
|
||||
function upload(path, filePath, formData = {}, name = "file") {
|
||||
return new Promise((resolve, reject) => {
|
||||
const header = { "X-Shop-Id": common_config.SHOP_ID };
|
||||
if (common_config.ENABLE_DEFAULT_USER && common_config.DEFAULT_USER_ID)
|
||||
header["X-User-Id"] = common_config.DEFAULT_USER_ID;
|
||||
const options = { url: buildUrl(path), filePath, name, formData, header };
|
||||
uploadWithFallback(options, common_config.API_BASE_URL_CANDIDATES, 0, resolve, reject);
|
||||
});
|
||||
}
|
||||
exports.del = del;
|
||||
exports.get = get;
|
||||
exports.post = post;
|
||||
exports.put = put;
|
||||
exports.upload = upload;
|
||||
//# sourceMappingURL=../../.sourcemap/mp-weixin/common/http.js.map
|
||||
|
||||
@@ -68,8 +68,8 @@ const capitalize = cacheStringFunction((str) => {
|
||||
return str.charAt(0).toUpperCase() + str.slice(1);
|
||||
});
|
||||
const toHandlerKey = cacheStringFunction((str) => {
|
||||
const s = str ? `on${capitalize(str)}` : ``;
|
||||
return s;
|
||||
const s2 = str ? `on${capitalize(str)}` : ``;
|
||||
return s2;
|
||||
});
|
||||
const hasChanged = (value, oldValue) => !Object.is(value, oldValue);
|
||||
const invokeArrayFns$1 = (fns, arg) => {
|
||||
@@ -92,6 +92,36 @@ const toNumber = (val) => {
|
||||
const n = isString(val) ? Number(val) : NaN;
|
||||
return isNaN(n) ? val : n;
|
||||
};
|
||||
function normalizeStyle(value) {
|
||||
if (isArray(value)) {
|
||||
const res = {};
|
||||
for (let i = 0; i < value.length; i++) {
|
||||
const item = value[i];
|
||||
const normalized = isString(item) ? parseStringStyle(item) : normalizeStyle(item);
|
||||
if (normalized) {
|
||||
for (const key in normalized) {
|
||||
res[key] = normalized[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
return res;
|
||||
} else if (isString(value) || isObject(value)) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
const listDelimiterRE = /;(?![^(]*\))/g;
|
||||
const propertyDelimiterRE = /:([^]+)/;
|
||||
const styleCommentRE = /\/\*[^]*?\*\//g;
|
||||
function parseStringStyle(cssText) {
|
||||
const ret = {};
|
||||
cssText.replace(styleCommentRE, "").split(listDelimiterRE).forEach((item) => {
|
||||
if (item) {
|
||||
const tmp = item.split(propertyDelimiterRE);
|
||||
tmp.length > 1 && (ret[tmp[0].trim()] = tmp[1].trim());
|
||||
}
|
||||
});
|
||||
return ret;
|
||||
}
|
||||
const toDisplayString = (val) => {
|
||||
return isString(val) ? val : val == null ? "" : isArray(val) || isObject(val) && (val.toString === objectToString || !isFunction(val.toString)) ? JSON.stringify(val, replacer, 2) : String(val);
|
||||
};
|
||||
@@ -1287,6 +1317,9 @@ function isReadonly(value) {
|
||||
function isShallow(value) {
|
||||
return !!(value && value["__v_isShallow"]);
|
||||
}
|
||||
function isProxy(value) {
|
||||
return isReactive(value) || isReadonly(value);
|
||||
}
|
||||
function toRaw(observed) {
|
||||
const raw = observed && observed["__v_raw"];
|
||||
return raw ? toRaw(raw) : observed;
|
||||
@@ -2078,6 +2111,47 @@ function setCurrentRenderingInstance(instance) {
|
||||
instance && instance.type.__scopeId || null;
|
||||
return prev;
|
||||
}
|
||||
const COMPONENTS = "components";
|
||||
function resolveComponent(name, maybeSelfReference) {
|
||||
return resolveAsset(COMPONENTS, name, true, maybeSelfReference) || name;
|
||||
}
|
||||
function resolveAsset(type, name, warnMissing = true, maybeSelfReference = false) {
|
||||
const instance = currentRenderingInstance || currentInstance;
|
||||
if (instance) {
|
||||
const Component2 = instance.type;
|
||||
if (type === COMPONENTS) {
|
||||
const selfName = getComponentName(
|
||||
Component2,
|
||||
false
|
||||
);
|
||||
if (selfName && (selfName === name || selfName === camelize(name) || selfName === capitalize(camelize(name)))) {
|
||||
return Component2;
|
||||
}
|
||||
}
|
||||
const res = (
|
||||
// local registration
|
||||
// check instance[type] first which is resolved for options API
|
||||
resolve(instance[type] || Component2[type], name) || // global registration
|
||||
resolve(instance.appContext[type], name)
|
||||
);
|
||||
if (!res && maybeSelfReference) {
|
||||
return Component2;
|
||||
}
|
||||
if (warnMissing && !res) {
|
||||
const extra = type === COMPONENTS ? `
|
||||
If this is a native custom element, make sure to exclude it from component resolution via compilerOptions.isCustomElement.` : ``;
|
||||
warn$1(`Failed to resolve ${type.slice(0, -1)}: ${name}${extra}`);
|
||||
}
|
||||
return res;
|
||||
} else {
|
||||
warn$1(
|
||||
`resolve${capitalize(type.slice(0, -1))} can only be used in render() or setup().`
|
||||
);
|
||||
}
|
||||
}
|
||||
function resolve(registry, name) {
|
||||
return registry && (registry[name] || registry[camelize(name)] || registry[capitalize(camelize(name))]);
|
||||
}
|
||||
const INITIAL_WATCHER_VALUE = {};
|
||||
function watch(source, cb, options) {
|
||||
if (!isFunction(cb)) {
|
||||
@@ -3692,6 +3766,12 @@ const Static = Symbol.for("v-stc");
|
||||
function isVNode(value) {
|
||||
return value ? value.__v_isVNode === true : false;
|
||||
}
|
||||
const InternalObjectKey = `__vInternal`;
|
||||
function guardReactiveProps(props) {
|
||||
if (!props)
|
||||
return null;
|
||||
return isProxy(props) || InternalObjectKey in props ? extend({}, props) : props;
|
||||
}
|
||||
const emptyAppContext = createAppContext();
|
||||
let uid = 0;
|
||||
function createComponentInstance(vnode, parent, suspense) {
|
||||
@@ -4932,6 +5012,11 @@ function initApp(app) {
|
||||
}
|
||||
}
|
||||
const propsCaches = /* @__PURE__ */ Object.create(null);
|
||||
function renderProps(props) {
|
||||
const { uid: uid2, __counter } = getCurrentInstance();
|
||||
const propsId = (propsCaches[uid2] || (propsCaches[uid2] = [])).push(guardReactiveProps(props)) - 1;
|
||||
return uid2 + "," + propsId + "," + __counter;
|
||||
}
|
||||
function pruneComponentPropsCache(uid2) {
|
||||
delete propsCaches[uid2];
|
||||
}
|
||||
@@ -4972,6 +5057,22 @@ function getCreateApp() {
|
||||
return my[method];
|
||||
}
|
||||
}
|
||||
function stringifyStyle(value) {
|
||||
if (isString(value)) {
|
||||
return value;
|
||||
}
|
||||
return stringify(normalizeStyle(value));
|
||||
}
|
||||
function stringify(styles) {
|
||||
let ret = "";
|
||||
if (!styles || isString(styles)) {
|
||||
return ret;
|
||||
}
|
||||
for (const key in styles) {
|
||||
ret += `${key.startsWith(`--`) ? key : hyphenate(key)}:${styles[key]};`;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
function vOn(value, key) {
|
||||
const instance = getCurrentInstance();
|
||||
const ctx = instance.ctx;
|
||||
@@ -5121,8 +5222,10 @@ function withModelModifiers(fn, { number, trim }, isComponent = false) {
|
||||
}
|
||||
const o = (value, key) => vOn(value, key);
|
||||
const f = (source, renderItem) => vFor(source, renderItem);
|
||||
const s = (value) => stringifyStyle(value);
|
||||
const e = (target, ...sources) => extend(target, ...sources);
|
||||
const t = (val) => toDisplayString(val);
|
||||
const p = (props) => renderProps(props);
|
||||
const m = (fn, modifiers, isComponent = false) => withModelModifiers(fn, modifiers, isComponent);
|
||||
function createApp$1(rootComponent, rootProps = null) {
|
||||
rootComponent && (rootComponent.mpType = "app");
|
||||
@@ -5445,8 +5548,8 @@ function promisify$1(name, fn) {
|
||||
if (hasCallback(args)) {
|
||||
return wrapperReturnValue(name, invokeApi(name, fn, extend({}, args), rest));
|
||||
}
|
||||
return wrapperReturnValue(name, handlePromise(new Promise((resolve, reject) => {
|
||||
invokeApi(name, fn, extend({}, args, { success: resolve, fail: reject }), rest);
|
||||
return wrapperReturnValue(name, handlePromise(new Promise((resolve2, reject) => {
|
||||
invokeApi(name, fn, extend({}, args, { success: resolve2, fail: reject }), rest);
|
||||
})));
|
||||
};
|
||||
}
|
||||
@@ -5767,7 +5870,7 @@ function invokeGetPushCidCallbacks(cid2, errMsg) {
|
||||
getPushCidCallbacks.length = 0;
|
||||
}
|
||||
const API_GET_PUSH_CLIENT_ID = "getPushClientId";
|
||||
const getPushClientId = defineAsyncApi(API_GET_PUSH_CLIENT_ID, (_, { resolve, reject }) => {
|
||||
const getPushClientId = defineAsyncApi(API_GET_PUSH_CLIENT_ID, (_, { resolve: resolve2, reject }) => {
|
||||
Promise.resolve().then(() => {
|
||||
if (typeof enabled === "undefined") {
|
||||
enabled = false;
|
||||
@@ -5776,7 +5879,7 @@ const getPushClientId = defineAsyncApi(API_GET_PUSH_CLIENT_ID, (_, { resolve, re
|
||||
}
|
||||
getPushCidCallbacks.push((cid2, errMsg) => {
|
||||
if (cid2) {
|
||||
resolve({ cid: cid2 });
|
||||
resolve2({ cid: cid2 });
|
||||
} else {
|
||||
reject(errMsg);
|
||||
}
|
||||
@@ -5845,9 +5948,9 @@ function promisify(name, api) {
|
||||
if (isFunction(options.success) || isFunction(options.fail) || isFunction(options.complete)) {
|
||||
return wrapperReturnValue(name, invokeApi(name, api, extend({}, options), rest));
|
||||
}
|
||||
return wrapperReturnValue(name, handlePromise(new Promise((resolve, reject) => {
|
||||
return wrapperReturnValue(name, handlePromise(new Promise((resolve2, reject) => {
|
||||
invokeApi(name, api, extend({}, options, {
|
||||
success: resolve,
|
||||
success: resolve2,
|
||||
fail: reject
|
||||
}), rest);
|
||||
})));
|
||||
@@ -6454,13 +6557,13 @@ function initRuntimeSocket(hosts, port, id) {
|
||||
}
|
||||
const SOCKET_TIMEOUT = 500;
|
||||
function tryConnectSocket(host2, port, id) {
|
||||
return new Promise((resolve, reject) => {
|
||||
return new Promise((resolve2, reject) => {
|
||||
const socket = index.connectSocket({
|
||||
url: `ws://${host2}:${port}/${id}`,
|
||||
multiple: true,
|
||||
// 支付宝小程序 是否开启多实例
|
||||
fail() {
|
||||
resolve(null);
|
||||
resolve2(null);
|
||||
}
|
||||
});
|
||||
const timer = setTimeout(() => {
|
||||
@@ -6468,19 +6571,19 @@ function tryConnectSocket(host2, port, id) {
|
||||
code: 1006,
|
||||
reason: "connect timeout"
|
||||
});
|
||||
resolve(null);
|
||||
resolve2(null);
|
||||
}, SOCKET_TIMEOUT);
|
||||
socket.onOpen((e2) => {
|
||||
clearTimeout(timer);
|
||||
resolve(socket);
|
||||
resolve2(socket);
|
||||
});
|
||||
socket.onClose((e2) => {
|
||||
clearTimeout(timer);
|
||||
resolve(null);
|
||||
resolve2(null);
|
||||
});
|
||||
socket.onError((e2) => {
|
||||
clearTimeout(timer);
|
||||
resolve(null);
|
||||
resolve2(null);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -7898,5 +8001,8 @@ exports.f = f;
|
||||
exports.index = index;
|
||||
exports.m = m;
|
||||
exports.o = o;
|
||||
exports.p = p;
|
||||
exports.resolveComponent = resolveComponent;
|
||||
exports.s = s;
|
||||
exports.t = t;
|
||||
//# sourceMappingURL=../../.sourcemap/mp-weixin/common/vendor.js.map
|
||||
|
||||
144
frontend/unpackage/dist/dev/mp-weixin/components/ImageUploader.js
vendored
Normal file
144
frontend/unpackage/dist/dev/mp-weixin/components/ImageUploader.js
vendored
Normal file
@@ -0,0 +1,144 @@
|
||||
"use strict";
|
||||
const common_vendor = require("../common/vendor.js");
|
||||
const common_http = require("../common/http.js");
|
||||
const ITEM_SIZE = 210;
|
||||
const GAP = 18;
|
||||
const COLS = 3;
|
||||
function px(rpx) {
|
||||
return rpx;
|
||||
}
|
||||
const _sfc_main = {
|
||||
name: "ImageUploader",
|
||||
props: {
|
||||
modelValue: { type: Array, default: () => [] },
|
||||
max: { type: Number, default: 9 },
|
||||
uploadPath: { type: String, default: "/api/attachments" },
|
||||
uploadFieldName: { type: String, default: "file" },
|
||||
formData: { type: Object, default: () => ({ ownerType: "product" }) }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
innerList: []
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
areaHeight() {
|
||||
const rows = Math.ceil((this.innerList.length + 1) / COLS) || 1;
|
||||
return rows * ITEM_SIZE + (rows - 1) * GAP;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
modelValue: {
|
||||
immediate: true,
|
||||
handler(list) {
|
||||
const mapped = (list || []).map((u, i) => ({
|
||||
uid: String(i) + "_" + (u.id || u.url || Math.random().toString(36).slice(2)),
|
||||
url: typeof u === "string" ? u : u.url || "",
|
||||
x: this.posOf(i).x,
|
||||
y: this.posOf(i).y
|
||||
}));
|
||||
this.innerList = mapped;
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
posOf(index) {
|
||||
const row = Math.floor(index / COLS);
|
||||
const col = index % COLS;
|
||||
return { x: px(col * (ITEM_SIZE + GAP)), y: px(row * (ITEM_SIZE + GAP)) };
|
||||
},
|
||||
cellStyle(index) {
|
||||
return {
|
||||
width: ITEM_SIZE + "rpx",
|
||||
height: ITEM_SIZE + "rpx"
|
||||
};
|
||||
},
|
||||
preview(index) {
|
||||
common_vendor.index.previewImage({ urls: this.innerList.map((i) => i.url), current: index });
|
||||
},
|
||||
remove(index) {
|
||||
this.innerList.splice(index, 1);
|
||||
this.reflow();
|
||||
this.emit();
|
||||
},
|
||||
choose() {
|
||||
const remain = this.max - this.innerList.length;
|
||||
if (remain <= 0)
|
||||
return;
|
||||
common_vendor.index.chooseImage({ count: remain, success: async (res) => {
|
||||
for (const path of res.tempFilePaths) {
|
||||
await this.doUpload(path);
|
||||
}
|
||||
} });
|
||||
},
|
||||
async doUpload(filePath) {
|
||||
var _a;
|
||||
try {
|
||||
const resp = await common_http.upload(this.uploadPath, filePath, this.formData, this.uploadFieldName);
|
||||
const url = (resp == null ? void 0 : resp.url) || ((_a = resp == null ? void 0 : resp.data) == null ? void 0 : _a.url) || (resp == null ? void 0 : resp.path) || "";
|
||||
if (!url)
|
||||
throw new Error("上传响应无 url");
|
||||
this.innerList.push({ uid: Math.random().toString(36).slice(2), url, ...this.posOf(this.innerList.length) });
|
||||
this.reflow();
|
||||
this.emit();
|
||||
} catch (e) {
|
||||
common_vendor.index.showToast({ title: "上传失败", icon: "none" });
|
||||
}
|
||||
},
|
||||
onMoving(index, e) {
|
||||
const { x, y } = e.detail;
|
||||
this.innerList[index].x = x;
|
||||
this.innerList[index].y = y;
|
||||
},
|
||||
onMoveEnd(index) {
|
||||
const mv = this.innerList[index];
|
||||
const col = Math.round(mv.x / (ITEM_SIZE + GAP));
|
||||
const row = Math.round(mv.y / (ITEM_SIZE + GAP));
|
||||
let newIndex = row * COLS + col;
|
||||
newIndex = Math.max(0, Math.min(newIndex, this.innerList.length - 1));
|
||||
if (newIndex !== index) {
|
||||
const moved = this.innerList.splice(index, 1)[0];
|
||||
this.innerList.splice(newIndex, 0, moved);
|
||||
}
|
||||
this.reflow();
|
||||
this.emit();
|
||||
},
|
||||
reflow() {
|
||||
this.innerList.forEach((it, i) => {
|
||||
const p = this.posOf(i);
|
||||
it.x = p.x;
|
||||
it.y = p.y;
|
||||
});
|
||||
},
|
||||
emit() {
|
||||
this.$emit("update:modelValue", this.innerList.map((i) => i.url));
|
||||
this.$emit("change", this.innerList.map((i) => i.url));
|
||||
}
|
||||
}
|
||||
};
|
||||
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
|
||||
return common_vendor.e({
|
||||
a: common_vendor.f($data.innerList, (img, index, i0) => {
|
||||
return {
|
||||
a: img.url,
|
||||
b: common_vendor.o(($event) => $options.preview(index), img.uid),
|
||||
c: common_vendor.o(($event) => $options.remove(index), img.uid),
|
||||
d: img.uid,
|
||||
e: common_vendor.s($options.cellStyle(index)),
|
||||
f: img.x,
|
||||
g: img.y,
|
||||
h: common_vendor.o(($event) => $options.onMoving(index, $event), img.uid),
|
||||
i: common_vendor.o(($event) => $options.onMoveEnd(index), img.uid)
|
||||
};
|
||||
}),
|
||||
b: $data.innerList.length < $props.max
|
||||
}, $data.innerList.length < $props.max ? {
|
||||
c: common_vendor.o((...args) => $options.choose && $options.choose(...args))
|
||||
} : {}, {
|
||||
d: $options.areaHeight + "rpx",
|
||||
e: $options.areaHeight + "rpx"
|
||||
});
|
||||
}
|
||||
const Component = /* @__PURE__ */ common_vendor._export_sfc(_sfc_main, [["render", _sfc_render]]);
|
||||
wx.createComponent(Component);
|
||||
//# sourceMappingURL=../../.sourcemap/mp-weixin/components/ImageUploader.js.map
|
||||
4
frontend/unpackage/dist/dev/mp-weixin/components/ImageUploader.json
vendored
Normal file
4
frontend/unpackage/dist/dev/mp-weixin/components/ImageUploader.json
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {}
|
||||
}
|
||||
1
frontend/unpackage/dist/dev/mp-weixin/components/ImageUploader.wxml
vendored
Normal file
1
frontend/unpackage/dist/dev/mp-weixin/components/ImageUploader.wxml
vendored
Normal file
@@ -0,0 +1 @@
|
||||
<view class="uploader"><view class="grid" style="{{'height:' + e}}"><movable-area class="area" style="{{'height:' + d}}"><movable-view wx:for="{{a}}" wx:for-item="img" wx:key="d" class="cell" style="{{img.e}}" direction="{{'all'}}" damping="{{40}}" friction="{{2}}" x="{{img.f}}" y="{{img.g}}" bindchange="{{img.h}}" bindtouchend="{{img.i}}"><image src="{{img.a}}" mode="aspectFill" class="thumb" bindtap="{{img.b}}"/><view class="remove" catchtap="{{img.c}}">×</view></movable-view><view wx:if="{{b}}" class="adder" bindtap="{{c}}"><text>+</text></view></movable-area></view></view>
|
||||
15
frontend/unpackage/dist/dev/mp-weixin/components/ImageUploader.wxss
vendored
Normal file
15
frontend/unpackage/dist/dev/mp-weixin/components/ImageUploader.wxss
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
|
||||
.uploader { padding: 12rpx; background: #fff;
|
||||
}
|
||||
.grid { position: relative;
|
||||
}
|
||||
.area { width: 100%; position: relative;
|
||||
}
|
||||
.cell { position: absolute; border-radius: 12rpx; overflow: hidden; box-shadow: 0 0 1rpx rgba(0,0,0,0.08);
|
||||
}
|
||||
.thumb { width: 100%; height: 100%;
|
||||
}
|
||||
.remove { position: absolute; right: 6rpx; top: 6rpx; background: rgba(0,0,0,0.45); color: #fff; width: 40rpx; height: 40rpx; text-align: center; line-height: 40rpx; border-radius: 20rpx; font-size: 28rpx;
|
||||
}
|
||||
.adder { width: 210rpx; height: 210rpx; border: 2rpx dashed #ccc; border-radius: 12rpx; display: flex; align-items: center; justify-content: center; color: #999; position: absolute; left: 0; top: 0;
|
||||
}
|
||||
@@ -11,6 +11,7 @@ const _sfc_main = {
|
||||
loadingNotices: false,
|
||||
noticeError: "",
|
||||
features: [
|
||||
{ key: "product", title: "货品", img: "/static/icons/product.png", emoji: "📦" },
|
||||
{ key: "customer", title: "客户", img: "/static/icons/customer.png", emoji: "👥" },
|
||||
{ key: "sale", title: "销售", img: "/static/icons/sale.png", emoji: "💰" },
|
||||
{ key: "account", title: "账户", img: "/static/icons/account.png", emoji: "💳" },
|
||||
@@ -30,12 +31,14 @@ const _sfc_main = {
|
||||
methods: {
|
||||
async fetchMetrics() {
|
||||
try {
|
||||
const d = await common_http.get("/api/metrics/overview");
|
||||
const d = await common_http.get("/api/dashboard/overview");
|
||||
const toNum = (v) => typeof v === "number" ? v : Number(v || 0);
|
||||
this.kpi = {
|
||||
todaySales: d && d.todaySales || "0.00",
|
||||
monthSales: d && d.monthSales || "0.00",
|
||||
monthProfit: d && d.monthProfit || "0.00",
|
||||
stockCount: d && d.stockCount || "0"
|
||||
...this.kpi,
|
||||
todaySales: toNum(d && d.todaySalesAmount).toFixed(2),
|
||||
monthSales: toNum(d && d.monthSalesAmount).toFixed(2),
|
||||
monthProfit: toNum(d && d.monthGrossProfit).toFixed(2),
|
||||
stockCount: String((d && d.stockTotalQuantity) != null ? d.stockTotalQuantity : 0)
|
||||
};
|
||||
} catch (e) {
|
||||
}
|
||||
@@ -56,8 +59,16 @@ const _sfc_main = {
|
||||
}
|
||||
},
|
||||
onFeatureTap(item) {
|
||||
if (item.key === "product") {
|
||||
common_vendor.index.navigateTo({ url: "/pages/product/list" });
|
||||
return;
|
||||
}
|
||||
common_vendor.index.showToast({ title: item.title + "(开发中)", icon: "none" });
|
||||
},
|
||||
goProduct() {
|
||||
this.activeTab = "product";
|
||||
common_vendor.index.navigateTo({ url: "/pages/product/list" });
|
||||
},
|
||||
onCreateOrder() {
|
||||
common_vendor.index.navigateTo({ url: "/pages/order/create" });
|
||||
},
|
||||
@@ -116,7 +127,7 @@ function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
|
||||
l: $data.activeTab === "home" ? 1 : "",
|
||||
m: common_vendor.o(($event) => $data.activeTab = "home"),
|
||||
n: $data.activeTab === "product" ? 1 : "",
|
||||
o: common_vendor.o(($event) => $data.activeTab = "product"),
|
||||
o: common_vendor.o((...args) => $options.goProduct && $options.goProduct(...args)),
|
||||
p: common_vendor.o((...args) => $options.onCreateOrder && $options.onCreateOrder(...args)),
|
||||
q: $data.activeTab === "detail" ? 1 : "",
|
||||
r: common_vendor.o(($event) => $data.activeTab = "detail"),
|
||||
|
||||
62
frontend/unpackage/dist/dev/mp-weixin/pages/product/categories.js
vendored
Normal file
62
frontend/unpackage/dist/dev/mp-weixin/pages/product/categories.js
vendored
Normal file
@@ -0,0 +1,62 @@
|
||||
"use strict";
|
||||
const common_vendor = require("../../common/vendor.js");
|
||||
const common_http = require("../../common/http.js");
|
||||
const _sfc_main = {
|
||||
data() {
|
||||
return { name: "", list: [] };
|
||||
},
|
||||
onLoad() {
|
||||
this.reload();
|
||||
},
|
||||
methods: {
|
||||
async reload() {
|
||||
try {
|
||||
const res = await common_http.get("/api/product-categories");
|
||||
this.list = Array.isArray(res == null ? void 0 : res.list) ? res.list : Array.isArray(res) ? res : [];
|
||||
} catch (_) {
|
||||
}
|
||||
},
|
||||
async create() {
|
||||
if (!this.name)
|
||||
return;
|
||||
await common_http.post("/api/product-categories", { name: this.name });
|
||||
this.name = "";
|
||||
this.reload();
|
||||
},
|
||||
async update(c) {
|
||||
await common_http.put("/api/product-categories/" + c.id, { name: c.name });
|
||||
common_vendor.index.showToast({ title: "已保存", icon: "success" });
|
||||
},
|
||||
async remove(c) {
|
||||
common_vendor.index.showModal({ content: "确定删除该类别?", success: async (r) => {
|
||||
if (!r.confirm)
|
||||
return;
|
||||
await common_http.del("/api/product-categories/" + c.id);
|
||||
this.reload();
|
||||
} });
|
||||
}
|
||||
}
|
||||
};
|
||||
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
|
||||
return {
|
||||
a: $data.name,
|
||||
b: common_vendor.o(common_vendor.m(($event) => $data.name = $event.detail.value, {
|
||||
trim: true
|
||||
})),
|
||||
c: common_vendor.o((...args) => $options.create && $options.create(...args)),
|
||||
d: common_vendor.f($data.list, (c, k0, i0) => {
|
||||
return {
|
||||
a: c.name,
|
||||
b: common_vendor.o(common_vendor.m(($event) => c.name = $event.detail.value, {
|
||||
trim: true
|
||||
}), c.id),
|
||||
c: common_vendor.o(($event) => $options.update(c), c.id),
|
||||
d: common_vendor.o(($event) => $options.remove(c), c.id),
|
||||
e: c.id
|
||||
};
|
||||
})
|
||||
};
|
||||
}
|
||||
const MiniProgramPage = /* @__PURE__ */ common_vendor._export_sfc(_sfc_main, [["render", _sfc_render]]);
|
||||
wx.createPage(MiniProgramPage);
|
||||
//# sourceMappingURL=../../../.sourcemap/mp-weixin/pages/product/categories.js.map
|
||||
4
frontend/unpackage/dist/dev/mp-weixin/pages/product/categories.json
vendored
Normal file
4
frontend/unpackage/dist/dev/mp-weixin/pages/product/categories.json
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"navigationBarTitleText": "类别管理",
|
||||
"usingComponents": {}
|
||||
}
|
||||
1
frontend/unpackage/dist/dev/mp-weixin/pages/product/categories.wxml
vendored
Normal file
1
frontend/unpackage/dist/dev/mp-weixin/pages/product/categories.wxml
vendored
Normal file
@@ -0,0 +1 @@
|
||||
<view class="page"><view class="toolbar"><input placeholder="新类别名称" value="{{a}}" bindinput="{{b}}"/><button size="mini" bindtap="{{c}}">新增</button></view><scroll-view scroll-y class="list"><view wx:for="{{d}}" wx:for-item="c" wx:key="e" class="item"><input value="{{c.a}}" bindinput="{{c.b}}"/><view class="ops"><button size="mini" bindtap="{{c.c}}">保存</button><button size="mini" type="warn" bindtap="{{c.d}}">删除</button></view></view></scroll-view></view>
|
||||
15
frontend/unpackage/dist/dev/mp-weixin/pages/product/categories.wxss
vendored
Normal file
15
frontend/unpackage/dist/dev/mp-weixin/pages/product/categories.wxss
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
|
||||
.page { display:flex; flex-direction: column; height: 100vh;
|
||||
}
|
||||
.toolbar { display:flex; gap: 12rpx; padding: 16rpx; background:#fff;
|
||||
}
|
||||
.toolbar input { flex:1; background:#f6f6f6; border-radius: 12rpx; padding: 12rpx;
|
||||
}
|
||||
.list { flex:1;
|
||||
}
|
||||
.item { display:flex; gap: 12rpx; align-items:center; padding: 16rpx; background:#fff; border-bottom: 1rpx solid #f1f1f1;
|
||||
}
|
||||
.item input { flex:1; background:#f7f7f7; border-radius: 10rpx; padding: 12rpx;
|
||||
}
|
||||
.ops { display:flex; gap: 10rpx;
|
||||
}
|
||||
251
frontend/unpackage/dist/dev/mp-weixin/pages/product/edit.js
vendored
Normal file
251
frontend/unpackage/dist/dev/mp-weixin/pages/product/edit.js
vendored
Normal file
@@ -0,0 +1,251 @@
|
||||
"use strict";
|
||||
const common_vendor = require("../../common/vendor.js");
|
||||
const common_http = require("../../common/http.js");
|
||||
function parseSafeStock(text) {
|
||||
const m = String(text || "").match(/\s*(\d+(?:\.\d+)?)\s*(?:-|~|—|到|,)?\s*(\d+(?:\.\d+)?)?\s*/);
|
||||
if (!m)
|
||||
return { min: null, max: null };
|
||||
return { min: m[1] ? Number(m[1]) : null, max: m[2] ? Number(m[2]) : null };
|
||||
}
|
||||
const _sfc_main = {
|
||||
data() {
|
||||
return {
|
||||
id: null,
|
||||
units: [],
|
||||
categories: [],
|
||||
form: {
|
||||
name: "",
|
||||
categoryId: null,
|
||||
unitId: null,
|
||||
barcode: "",
|
||||
brand: "",
|
||||
model: "",
|
||||
spec: "",
|
||||
purchasePrice: 0,
|
||||
wholesalePrice: 0,
|
||||
bigClientPrice: 0,
|
||||
retailPrice: 0,
|
||||
stock: 0,
|
||||
safeMin: null,
|
||||
safeMax: null,
|
||||
images: []
|
||||
}
|
||||
};
|
||||
},
|
||||
onLoad(query) {
|
||||
this.id = query && query.id ? Number(query.id) : null;
|
||||
this.bootstrap();
|
||||
},
|
||||
computed: {
|
||||
categoryLabel() {
|
||||
const c = this.categories.find((x) => x.id === this.form.categoryId);
|
||||
return c ? c.name : "未选择";
|
||||
},
|
||||
unitLabel() {
|
||||
const u = this.units.find((x) => x.id === this.form.unitId);
|
||||
return u ? u.name : "未选择";
|
||||
},
|
||||
safeStockText: {
|
||||
get() {
|
||||
const a = this.form.safeMin != null ? String(this.form.safeMin) : "";
|
||||
const b = this.form.safeMax != null ? String(this.form.safeMax) : "";
|
||||
if (!a && !b)
|
||||
return "";
|
||||
if (a && b)
|
||||
return a + "-" + b;
|
||||
return a || b;
|
||||
},
|
||||
set(v) {
|
||||
const { min, max } = parseSafeStock(v);
|
||||
this.form.safeMin = min;
|
||||
this.form.safeMax = max;
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async bootstrap() {
|
||||
try {
|
||||
const [units, cats] = await Promise.all([
|
||||
common_http.get("/api/product/units"),
|
||||
common_http.get("/api/product/categories")
|
||||
]);
|
||||
this.units = Array.isArray(units == null ? void 0 : units.list) ? units.list : Array.isArray(units) ? units : [];
|
||||
this.categories = Array.isArray(cats == null ? void 0 : cats.list) ? cats.list : Array.isArray(cats) ? cats : [];
|
||||
} catch (_) {
|
||||
}
|
||||
if (this.id) {
|
||||
try {
|
||||
const d = await common_http.get("/api/product/detail", { id: this.id });
|
||||
Object.assign(this.form, {
|
||||
name: d.name || "",
|
||||
categoryId: d.categoryId || null,
|
||||
unitId: d.unitId || null,
|
||||
barcode: d.barcode || "",
|
||||
brand: d.brand || "",
|
||||
model: d.model || "",
|
||||
spec: d.spec || "",
|
||||
purchasePrice: Number(d.purchasePrice || 0),
|
||||
wholesalePrice: Number(d.wholesalePrice || 0),
|
||||
bigClientPrice: Number(d.bigClientPrice || 0),
|
||||
retailPrice: Number(d.retailPrice || 0),
|
||||
stock: Number(d.stock || 0),
|
||||
safeMin: d.safeMin ?? null,
|
||||
safeMax: d.safeMax ?? null,
|
||||
images: Array.isArray(d.images) ? d.images : []
|
||||
});
|
||||
} catch (_) {
|
||||
}
|
||||
}
|
||||
},
|
||||
chooseCategory() {
|
||||
if (!this.categories.length)
|
||||
return;
|
||||
common_vendor.index.showActionSheet({ itemList: this.categories.map((c) => c.name), success: ({ tapIndex }) => {
|
||||
this.form.categoryId = this.categories[tapIndex].id;
|
||||
} });
|
||||
},
|
||||
chooseUnit() {
|
||||
if (!this.units.length)
|
||||
return;
|
||||
common_vendor.index.showActionSheet({ itemList: this.units.map((u) => u.name), success: ({ tapIndex }) => {
|
||||
this.form.unitId = this.units[tapIndex].id;
|
||||
} });
|
||||
},
|
||||
scanBarcode() {
|
||||
common_vendor.index.scanCode({ onlyFromCamera: false, success: (res) => {
|
||||
this.form.barcode = res.result;
|
||||
}, fail: () => {
|
||||
common_vendor.index.showToast({ title: "扫码失败,请手动录入", icon: "none" });
|
||||
} });
|
||||
},
|
||||
async chooseImages() {
|
||||
common_vendor.index.chooseImage({ count: 6, sizeType: ["compressed"], success: async (res) => {
|
||||
try {
|
||||
const uploaded = [];
|
||||
for (const path of res.tempFilePaths) {
|
||||
const ret = await common_http.upload("/api/upload", path, { biz: "product" });
|
||||
uploaded.push(ret.url || ret.path || path);
|
||||
}
|
||||
this.form.images = (this.form.images || []).concat(uploaded).slice(0, 9);
|
||||
} catch (e) {
|
||||
common_vendor.index.showToast({ title: "上传失败", icon: "none" });
|
||||
}
|
||||
} });
|
||||
},
|
||||
moveLeft(i) {
|
||||
if (i <= 0)
|
||||
return;
|
||||
const a = this.form.images;
|
||||
[a[i - 1], a[i]] = [a[i], a[i - 1]];
|
||||
},
|
||||
moveRight(i) {
|
||||
const a = this.form.images;
|
||||
if (i >= a.length - 1)
|
||||
return;
|
||||
[a[i + 1], a[i]] = [a[i], a[i + 1]];
|
||||
},
|
||||
removeImg(i) {
|
||||
this.form.images.splice(i, 1);
|
||||
},
|
||||
async submit() {
|
||||
if (!this.form.name) {
|
||||
common_vendor.index.showToast({ title: "请填写名称", icon: "none" });
|
||||
return;
|
||||
}
|
||||
const payload = {
|
||||
name: this.form.name,
|
||||
categoryId: this.form.categoryId,
|
||||
unitId: this.form.unitId,
|
||||
barcode: this.form.barcode,
|
||||
brand: this.form.brand,
|
||||
model: this.form.model,
|
||||
spec: this.form.spec,
|
||||
purchasePrice: Number(this.form.purchasePrice || 0),
|
||||
wholesalePrice: Number(this.form.wholesalePrice || 0),
|
||||
bigClientPrice: Number(this.form.bigClientPrice || 0),
|
||||
retailPrice: Number(this.form.retailPrice || 0),
|
||||
stock: Number(this.form.stock || 0),
|
||||
safeMin: this.form.safeMin,
|
||||
safeMax: this.form.safeMax,
|
||||
images: this.form.images
|
||||
};
|
||||
try {
|
||||
if (this.id)
|
||||
await common_http.put("/api/product", { id: this.id, ...payload });
|
||||
else
|
||||
await common_http.post("/api/product", payload);
|
||||
common_vendor.index.showToast({ title: "已保存", icon: "success" });
|
||||
setTimeout(() => {
|
||||
common_vendor.index.navigateBack();
|
||||
}, 600);
|
||||
} catch (e) {
|
||||
common_vendor.index.showToast({ title: e && e.message || "保存失败", icon: "none" });
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
|
||||
return {
|
||||
a: $data.form.name,
|
||||
b: common_vendor.o(common_vendor.m(($event) => $data.form.name = $event.detail.value, {
|
||||
trim: true
|
||||
})),
|
||||
c: common_vendor.t($options.categoryLabel),
|
||||
d: common_vendor.o((...args) => $options.chooseCategory && $options.chooseCategory(...args)),
|
||||
e: common_vendor.t($options.unitLabel),
|
||||
f: common_vendor.o((...args) => $options.chooseUnit && $options.chooseUnit(...args)),
|
||||
g: $data.form.barcode,
|
||||
h: common_vendor.o(common_vendor.m(($event) => $data.form.barcode = $event.detail.value, {
|
||||
trim: true
|
||||
})),
|
||||
i: common_vendor.o((...args) => $options.scanBarcode && $options.scanBarcode(...args)),
|
||||
j: $data.form.brand,
|
||||
k: common_vendor.o(common_vendor.m(($event) => $data.form.brand = $event.detail.value, {
|
||||
trim: true
|
||||
})),
|
||||
l: $data.form.model,
|
||||
m: common_vendor.o(common_vendor.m(($event) => $data.form.model = $event.detail.value, {
|
||||
trim: true
|
||||
})),
|
||||
n: $data.form.spec,
|
||||
o: common_vendor.o(common_vendor.m(($event) => $data.form.spec = $event.detail.value, {
|
||||
trim: true
|
||||
})),
|
||||
p: $data.form.purchasePrice,
|
||||
q: common_vendor.o(common_vendor.m(($event) => $data.form.purchasePrice = $event.detail.value, {
|
||||
number: true
|
||||
})),
|
||||
r: $data.form.wholesalePrice,
|
||||
s: common_vendor.o(common_vendor.m(($event) => $data.form.wholesalePrice = $event.detail.value, {
|
||||
number: true
|
||||
})),
|
||||
t: $data.form.bigClientPrice,
|
||||
v: common_vendor.o(common_vendor.m(($event) => $data.form.bigClientPrice = $event.detail.value, {
|
||||
number: true
|
||||
})),
|
||||
w: $data.form.retailPrice,
|
||||
x: common_vendor.o(common_vendor.m(($event) => $data.form.retailPrice = $event.detail.value, {
|
||||
number: true
|
||||
})),
|
||||
y: $data.form.stock,
|
||||
z: common_vendor.o(common_vendor.m(($event) => $data.form.stock = $event.detail.value, {
|
||||
number: true
|
||||
})),
|
||||
A: $options.safeStockText,
|
||||
B: common_vendor.o(($event) => $options.safeStockText = $event.detail.value),
|
||||
C: common_vendor.o((...args) => $options.chooseImages && $options.chooseImages(...args)),
|
||||
D: common_vendor.f($data.form.images, (url, idx, i0) => {
|
||||
return {
|
||||
a: url,
|
||||
b: common_vendor.o(($event) => $options.moveLeft(idx), idx),
|
||||
c: common_vendor.o(($event) => $options.moveRight(idx), idx),
|
||||
d: common_vendor.o(($event) => $options.removeImg(idx), idx),
|
||||
e: idx
|
||||
};
|
||||
}),
|
||||
E: common_vendor.o((...args) => $options.submit && $options.submit(...args))
|
||||
};
|
||||
}
|
||||
const MiniProgramPage = /* @__PURE__ */ common_vendor._export_sfc(_sfc_main, [["render", _sfc_render]]);
|
||||
wx.createPage(MiniProgramPage);
|
||||
//# sourceMappingURL=../../../.sourcemap/mp-weixin/pages/product/edit.js.map
|
||||
4
frontend/unpackage/dist/dev/mp-weixin/pages/product/edit.json
vendored
Normal file
4
frontend/unpackage/dist/dev/mp-weixin/pages/product/edit.json
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"navigationBarTitleText": "编辑货品",
|
||||
"usingComponents": {}
|
||||
}
|
||||
1
frontend/unpackage/dist/dev/mp-weixin/pages/product/edit.wxml
vendored
Normal file
1
frontend/unpackage/dist/dev/mp-weixin/pages/product/edit.wxml
vendored
Normal file
@@ -0,0 +1 @@
|
||||
<view class="page"><view class="form"><view class="field"><text class="label">名称</text><input class="value" placeholder="必填" value="{{a}}" bindinput="{{b}}"/></view><view class="field" bindtap="{{d}}"><text class="label">类别</text><text class="value">{{c}}</text></view><view class="field" bindtap="{{f}}"><text class="label">主单位</text><text class="value">{{e}}</text></view><view class="field"><text class="label">条形码</text><input class="value" placeholder="手动录入或扫码" value="{{g}}" bindinput="{{h}}"/><button size="mini" class="scan" bindtap="{{i}}">扫码</button></view><view class="field"><text class="label">品牌</text><input class="value" placeholder="可选" value="{{j}}" bindinput="{{k}}"/></view><view class="field"><text class="label">型号</text><input class="value" placeholder="可选" value="{{l}}" bindinput="{{m}}"/></view><view class="field"><text class="label">规格</text><input class="value" placeholder="可选" value="{{n}}" bindinput="{{o}}"/></view><view class="grid2"><view class="field"><text class="label">进货价</text><input class="value" type="digit" value="{{p}}" bindinput="{{q}}"/></view><view class="field"><text class="label">批发价</text><input class="value" type="digit" value="{{r}}" bindinput="{{s}}"/></view><view class="field"><text class="label">大单报价</text><input class="value" type="digit" value="{{t}}" bindinput="{{v}}"/></view><view class="field"><text class="label">零售价</text><input class="value" type="digit" value="{{w}}" bindinput="{{x}}"/></view></view><view class="grid2"><view class="field"><text class="label">当前库存</text><input class="value" type="digit" value="{{y}}" bindinput="{{z}}"/></view><view class="field"><text class="label">安全库存</text><input class="value" type="text" placeholder="如 10-100" value="{{A}}" bindinput="{{B}}"/></view></view><view class="photos"><view class="photos-header"><text>商品图片</text><button size="mini" bindtap="{{C}}">选择图片</button></view><view class="photo-list"><view wx:for="{{D}}" wx:for-item="url" wx:key="e" class="photo"><image src="{{url.a}}" mode="aspectFill"/><view class="photo-actions"><text class="btn" bindtap="{{url.b}}">←</text><text class="btn" bindtap="{{url.c}}">→</text><text class="btn danger" bindtap="{{url.d}}">删</text></view></view></view></view></view><view class="bottom"><button class="primary" bindtap="{{E}}">保存</button></view></view>
|
||||
35
frontend/unpackage/dist/dev/mp-weixin/pages/product/edit.wxss
vendored
Normal file
35
frontend/unpackage/dist/dev/mp-weixin/pages/product/edit.wxss
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
|
||||
.page{ padding-bottom: 140rpx;
|
||||
}
|
||||
.form{ background:#fff;
|
||||
}
|
||||
.field{ display:flex; align-items:center; gap: 12rpx; padding: 22rpx 20rpx; border-bottom: 1rpx solid #f2f2f2;
|
||||
}
|
||||
.label{ color:#666; width: 160rpx;
|
||||
}
|
||||
.value{ flex:1; color:#333;
|
||||
}
|
||||
.scan{ flex: 0 0 auto;
|
||||
}
|
||||
.grid2{ display:grid; grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
.photos{ padding: 16rpx 20rpx;
|
||||
}
|
||||
.photos-header{ display:flex; justify-content: space-between; align-items:center; margin-bottom: 12rpx;
|
||||
}
|
||||
.photo-list{ display:grid; grid-template-columns: repeat(3, 1fr); gap: 12rpx;
|
||||
}
|
||||
.photo{ position: relative;
|
||||
}
|
||||
.photo image{ width: 100%; height: 200rpx; border-radius: 12rpx; background:#f6f6f6; display:block;
|
||||
}
|
||||
.photo-actions{ position:absolute; right:6rpx; bottom:6rpx; display:flex; gap: 6rpx;
|
||||
}
|
||||
.btn{ font-size: 22rpx; padding: 4rpx 8rpx; background: rgba(0,0,0,0.45); color:#fff; border-radius: 8rpx;
|
||||
}
|
||||
.btn.danger{ background: rgba(221,82,77,0.85);
|
||||
}
|
||||
.bottom{ position: fixed; left:0; right:0; bottom:0; background:#fff; padding: 16rpx 24rpx calc(env(safe-area-inset-bottom) + 16rpx); box-shadow: 0 -4rpx 12rpx rgba(0,0,0,0.06);
|
||||
}
|
||||
.primary{ width: 100%; background: linear-gradient(135deg, #FFE69A 0%, #F4CF62 45%, #D7A72E 100%); color:#493c1b; border-radius: 999rpx; padding: 20rpx 0; font-weight:800;
|
||||
}
|
||||
249
frontend/unpackage/dist/dev/mp-weixin/pages/product/form.js
vendored
Normal file
249
frontend/unpackage/dist/dev/mp-weixin/pages/product/form.js
vendored
Normal file
@@ -0,0 +1,249 @@
|
||||
"use strict";
|
||||
const common_vendor = require("../../common/vendor.js");
|
||||
const common_http = require("../../common/http.js");
|
||||
const ImageUploader = () => "../../components/ImageUploader.js";
|
||||
const _sfc_main = {
|
||||
components: { ImageUploader },
|
||||
data() {
|
||||
return {
|
||||
id: "",
|
||||
form: {
|
||||
name: "",
|
||||
barcode: "",
|
||||
brand: "",
|
||||
model: "",
|
||||
spec: "",
|
||||
origin: "",
|
||||
categoryId: "",
|
||||
unitId: "",
|
||||
stock: null,
|
||||
safeMin: null,
|
||||
safeMax: null,
|
||||
purchasePrice: null,
|
||||
retailPrice: null,
|
||||
wholesalePrice: null,
|
||||
bigClientPrice: null,
|
||||
images: [],
|
||||
remark: ""
|
||||
},
|
||||
units: [],
|
||||
categories: []
|
||||
};
|
||||
},
|
||||
onLoad(query) {
|
||||
this.id = (query == null ? void 0 : query.id) || "";
|
||||
this.bootstrap();
|
||||
},
|
||||
computed: {
|
||||
unitNames() {
|
||||
return this.units.map((u) => u.name);
|
||||
},
|
||||
categoryNames() {
|
||||
return this.categories.map((c) => c.name);
|
||||
},
|
||||
unitLabel() {
|
||||
const u = this.units.find((x) => String(x.id) === String(this.form.unitId));
|
||||
return u ? u.name : "选择单位";
|
||||
},
|
||||
categoryLabel() {
|
||||
const c = this.categories.find((x) => String(x.id) === String(this.form.categoryId));
|
||||
return c ? c.name : "选择类别";
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async bootstrap() {
|
||||
await Promise.all([this.fetchUnits(), this.fetchCategories()]);
|
||||
if (this.id)
|
||||
this.loadDetail();
|
||||
},
|
||||
async fetchUnits() {
|
||||
try {
|
||||
const res = await common_http.get("/api/product-units");
|
||||
this.units = Array.isArray(res == null ? void 0 : res.list) ? res.list : Array.isArray(res) ? res : [];
|
||||
} catch (_) {
|
||||
}
|
||||
},
|
||||
async fetchCategories() {
|
||||
try {
|
||||
const res = await common_http.get("/api/product-categories");
|
||||
this.categories = Array.isArray(res == null ? void 0 : res.list) ? res.list : Array.isArray(res) ? res : [];
|
||||
} catch (_) {
|
||||
}
|
||||
},
|
||||
onPickUnit(e) {
|
||||
const idx = Number(e.detail.value);
|
||||
const u = this.units[idx];
|
||||
this.form.unitId = u ? u.id : "";
|
||||
},
|
||||
onPickCategory(e) {
|
||||
const idx = Number(e.detail.value);
|
||||
const c = this.categories[idx];
|
||||
this.form.categoryId = c ? c.id : "";
|
||||
},
|
||||
scan() {
|
||||
common_vendor.index.scanCode({ onlyFromCamera: false, success: (res) => {
|
||||
this.form.barcode = res.result;
|
||||
} });
|
||||
},
|
||||
async loadDetail() {
|
||||
try {
|
||||
const data = await common_http.get("/api/products/" + this.id);
|
||||
Object.assign(this.form, {
|
||||
name: data.name,
|
||||
barcode: data.barcode,
|
||||
brand: data.brand,
|
||||
model: data.model,
|
||||
spec: data.spec,
|
||||
origin: data.origin,
|
||||
categoryId: data.categoryId,
|
||||
unitId: data.unitId,
|
||||
stock: data.stock,
|
||||
safeMin: data.safeMin,
|
||||
safeMax: data.safeMax,
|
||||
purchasePrice: data.purchasePrice,
|
||||
retailPrice: data.retailPrice,
|
||||
wholesalePrice: data.wholesalePrice,
|
||||
bigClientPrice: data.bigClientPrice,
|
||||
images: (data.images || []).map((i) => i.url || i)
|
||||
});
|
||||
} catch (_) {
|
||||
}
|
||||
},
|
||||
validate() {
|
||||
if (!this.form.name) {
|
||||
common_vendor.index.showToast({ title: "请填写名称", icon: "none" });
|
||||
return false;
|
||||
}
|
||||
if (this.form.safeMin != null && this.form.safeMax != null && Number(this.form.safeMin) > Number(this.form.safeMax)) {
|
||||
common_vendor.index.showToast({ title: "安全库存区间不合法", icon: "none" });
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
buildPayload() {
|
||||
const f = this.form;
|
||||
return {
|
||||
name: f.name,
|
||||
barcode: f.barcode,
|
||||
brand: f.brand,
|
||||
model: f.model,
|
||||
spec: f.spec,
|
||||
origin: f.origin,
|
||||
categoryId: f.categoryId || null,
|
||||
unitId: f.unitId,
|
||||
safeMin: f.safeMin,
|
||||
safeMax: f.safeMax,
|
||||
prices: {
|
||||
purchasePrice: f.purchasePrice,
|
||||
retailPrice: f.retailPrice,
|
||||
wholesalePrice: f.wholesalePrice,
|
||||
bigClientPrice: f.bigClientPrice
|
||||
},
|
||||
stock: f.stock,
|
||||
images: f.images,
|
||||
remark: f.remark
|
||||
};
|
||||
},
|
||||
async save(goOn) {
|
||||
if (!this.validate())
|
||||
return;
|
||||
const payload = this.buildPayload();
|
||||
try {
|
||||
if (this.id)
|
||||
await common_http.put("/api/products/" + this.id, payload);
|
||||
else
|
||||
await common_http.post("/api/products", payload);
|
||||
common_vendor.index.showToast({ title: "保存成功", icon: "success" });
|
||||
if (goOn && !this.id) {
|
||||
this.form = { name: "", barcode: "", brand: "", model: "", spec: "", origin: "", categoryId: "", unitId: "", stock: null, safeMin: null, safeMax: null, purchasePrice: null, retailPrice: null, wholesalePrice: null, bigClientPrice: null, images: [], remark: "" };
|
||||
} else {
|
||||
setTimeout(() => common_vendor.index.navigateBack(), 400);
|
||||
}
|
||||
} catch (e) {
|
||||
common_vendor.index.showToast({ title: "保存失败", icon: "none" });
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
if (!Array) {
|
||||
const _component_ImageUploader = common_vendor.resolveComponent("ImageUploader");
|
||||
_component_ImageUploader();
|
||||
}
|
||||
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
|
||||
return {
|
||||
a: $data.form.name,
|
||||
b: common_vendor.o(common_vendor.m(($event) => $data.form.name = $event.detail.value, {
|
||||
trim: true
|
||||
})),
|
||||
c: $data.form.barcode,
|
||||
d: common_vendor.o(common_vendor.m(($event) => $data.form.barcode = $event.detail.value, {
|
||||
trim: true
|
||||
})),
|
||||
e: $data.form.brand,
|
||||
f: common_vendor.o(common_vendor.m(($event) => $data.form.brand = $event.detail.value, {
|
||||
trim: true
|
||||
})),
|
||||
g: $data.form.model,
|
||||
h: common_vendor.o(common_vendor.m(($event) => $data.form.model = $event.detail.value, {
|
||||
trim: true
|
||||
})),
|
||||
i: $data.form.spec,
|
||||
j: common_vendor.o(common_vendor.m(($event) => $data.form.spec = $event.detail.value, {
|
||||
trim: true
|
||||
})),
|
||||
k: $data.form.origin,
|
||||
l: common_vendor.o(common_vendor.m(($event) => $data.form.origin = $event.detail.value, {
|
||||
trim: true
|
||||
})),
|
||||
m: common_vendor.t($options.unitLabel),
|
||||
n: $options.unitNames,
|
||||
o: common_vendor.o((...args) => $options.onPickUnit && $options.onPickUnit(...args)),
|
||||
p: common_vendor.t($options.categoryLabel),
|
||||
q: $options.categoryNames,
|
||||
r: common_vendor.o((...args) => $options.onPickCategory && $options.onPickCategory(...args)),
|
||||
s: $data.form.stock,
|
||||
t: common_vendor.o(common_vendor.m(($event) => $data.form.stock = $event.detail.value, {
|
||||
number: true
|
||||
})),
|
||||
v: $data.form.safeMin,
|
||||
w: common_vendor.o(common_vendor.m(($event) => $data.form.safeMin = $event.detail.value, {
|
||||
number: true
|
||||
})),
|
||||
x: $data.form.safeMax,
|
||||
y: common_vendor.o(common_vendor.m(($event) => $data.form.safeMax = $event.detail.value, {
|
||||
number: true
|
||||
})),
|
||||
z: $data.form.purchasePrice,
|
||||
A: common_vendor.o(common_vendor.m(($event) => $data.form.purchasePrice = $event.detail.value, {
|
||||
number: true
|
||||
})),
|
||||
B: $data.form.retailPrice,
|
||||
C: common_vendor.o(common_vendor.m(($event) => $data.form.retailPrice = $event.detail.value, {
|
||||
number: true
|
||||
})),
|
||||
D: $data.form.wholesalePrice,
|
||||
E: common_vendor.o(common_vendor.m(($event) => $data.form.wholesalePrice = $event.detail.value, {
|
||||
number: true
|
||||
})),
|
||||
F: $data.form.bigClientPrice,
|
||||
G: common_vendor.o(common_vendor.m(($event) => $data.form.bigClientPrice = $event.detail.value, {
|
||||
number: true
|
||||
})),
|
||||
H: common_vendor.o(($event) => $data.form.images = $event),
|
||||
I: common_vendor.p({
|
||||
formData: {
|
||||
ownerType: "product"
|
||||
},
|
||||
modelValue: $data.form.images
|
||||
}),
|
||||
J: $data.form.remark,
|
||||
K: common_vendor.o(common_vendor.m(($event) => $data.form.remark = $event.detail.value, {
|
||||
trim: true
|
||||
})),
|
||||
L: common_vendor.o(($event) => $options.save(false)),
|
||||
M: common_vendor.o(($event) => $options.save(true))
|
||||
};
|
||||
}
|
||||
const MiniProgramPage = /* @__PURE__ */ common_vendor._export_sfc(_sfc_main, [["render", _sfc_render]]);
|
||||
wx.createPage(MiniProgramPage);
|
||||
//# sourceMappingURL=../../../.sourcemap/mp-weixin/pages/product/form.js.map
|
||||
6
frontend/unpackage/dist/dev/mp-weixin/pages/product/form.json
vendored
Normal file
6
frontend/unpackage/dist/dev/mp-weixin/pages/product/form.json
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"navigationBarTitleText": "编辑货品",
|
||||
"usingComponents": {
|
||||
"image-uploader": "../../components/ImageUploader"
|
||||
}
|
||||
}
|
||||
1
frontend/unpackage/dist/dev/mp-weixin/pages/product/form.wxml
vendored
Normal file
1
frontend/unpackage/dist/dev/mp-weixin/pages/product/form.wxml
vendored
Normal file
@@ -0,0 +1 @@
|
||||
<scroll-view scroll-y class="page"><view class="card"><view class="row"><text class="label">商品名称</text><input placeholder="必填" value="{{a}}" bindinput="{{b}}"/></view><view class="row"><text class="label">条形码</text><input placeholder="可扫码或输入" value="{{c}}" bindinput="{{d}}"/></view><view class="row"><text class="label">品牌/型号/规格/产地</text></view><view class="row"><input placeholder="品牌" value="{{e}}" bindinput="{{f}}"/></view><view class="row"><input placeholder="型号" value="{{g}}" bindinput="{{h}}"/></view><view class="row"><input placeholder="规格" value="{{i}}" bindinput="{{j}}"/></view><view class="row"><input placeholder="产地" value="{{k}}" bindinput="{{l}}"/></view><view class="row"><picker mode="selector" range="{{n}}" bindchange="{{o}}"><view class="picker">主单位:{{m}}</view></picker><picker mode="selector" range="{{q}}" bindchange="{{r}}"><view class="picker">类别:{{p}}</view></picker></view></view><view class="card"><view class="row"><text class="label">库存与安全库存</text></view><view class="row"><input type="number" placeholder="当前库存" value="{{s}}" bindinput="{{t}}"/><input type="number" placeholder="安全库存下限" value="{{v}}" bindinput="{{w}}"/><input type="number" placeholder="安全库存上限" value="{{x}}" bindinput="{{y}}"/></view></view><view class="card"><view class="row"><text class="label">价格(进价/零售/批发/大单)</text></view><view class="row prices"><input type="number" placeholder="进货价" value="{{z}}" bindinput="{{A}}"/><input type="number" placeholder="零售价" value="{{B}}" bindinput="{{C}}"/><input type="number" placeholder="批发价" value="{{D}}" bindinput="{{E}}"/><input type="number" placeholder="大单价" value="{{F}}" bindinput="{{G}}"/></view></view><view class="card"><text class="label">图片</text><image-uploader wx:if="{{I}}" u-i="4a3f460a-0" bind:__l="__l" bindupdateModelValue="{{H}}" u-p="{{I}}"/></view><view class="card"><text class="label">备注</text><block wx:if="{{r0}}"><textarea placeholder="可选" auto-height value="{{J}}" bindinput="{{K}}"/></block></view><view class="fixed"><button type="default" bindtap="{{L}}">保存</button><button type="primary" bindtap="{{M}}">保存并继续</button></view></scroll-view>
|
||||
17
frontend/unpackage/dist/dev/mp-weixin/pages/product/form.wxss
vendored
Normal file
17
frontend/unpackage/dist/dev/mp-weixin/pages/product/form.wxss
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
|
||||
.page { background:#f6f6f6; height: 100vh;
|
||||
}
|
||||
.card { background:#fff; margin: 16rpx; padding: 16rpx; border-radius: 12rpx;
|
||||
}
|
||||
.row { display:flex; gap: 12rpx; align-items: center; margin-bottom: 12rpx;
|
||||
}
|
||||
.label { width: 180rpx; color:#666;
|
||||
}
|
||||
.row input { flex:1; background:#f7f7f7; border-radius: 10rpx; padding: 12rpx;
|
||||
}
|
||||
.picker { padding: 8rpx 12rpx; background:#f0f0f0; border-radius: 10rpx; color:#666; margin-left: 8rpx;
|
||||
}
|
||||
.prices input { width: 30%;
|
||||
}
|
||||
.fixed { position: fixed; left: 0; right: 0; bottom: 0; background:#fff; padding: 12rpx 16rpx; display:flex; gap: 16rpx;
|
||||
}
|
||||
122
frontend/unpackage/dist/dev/mp-weixin/pages/product/list.js
vendored
Normal file
122
frontend/unpackage/dist/dev/mp-weixin/pages/product/list.js
vendored
Normal file
@@ -0,0 +1,122 @@
|
||||
"use strict";
|
||||
const common_vendor = require("../../common/vendor.js");
|
||||
const common_http = require("../../common/http.js");
|
||||
const _sfc_main = {
|
||||
data() {
|
||||
return {
|
||||
items: [],
|
||||
query: { kw: "", page: 1, size: 20, categoryId: "" },
|
||||
finished: false,
|
||||
loading: false,
|
||||
tab: "all",
|
||||
categories: []
|
||||
};
|
||||
},
|
||||
onLoad() {
|
||||
this.fetchCategories();
|
||||
this.reload();
|
||||
},
|
||||
computed: {
|
||||
categoryNames() {
|
||||
return this.categories.map((c) => c.name);
|
||||
},
|
||||
categoryLabel() {
|
||||
const c = this.categories.find((x) => String(x.id) === String(this.query.categoryId));
|
||||
return c ? "类别:" + c.name : "选择类别";
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
switchTab(t) {
|
||||
this.tab = t;
|
||||
this.query.categoryId = "";
|
||||
this.reload();
|
||||
},
|
||||
onPickCategory(e) {
|
||||
const idx = Number(e.detail.value);
|
||||
const c = this.categories[idx];
|
||||
this.query.categoryId = c ? c.id : "";
|
||||
this.reload();
|
||||
},
|
||||
async fetchCategories() {
|
||||
try {
|
||||
const res = await common_http.get("/api/product-categories", {});
|
||||
this.categories = Array.isArray(res == null ? void 0 : res.list) ? res.list : Array.isArray(res) ? res : [];
|
||||
} catch (_) {
|
||||
}
|
||||
},
|
||||
reload() {
|
||||
this.items = [];
|
||||
this.query.page = 1;
|
||||
this.finished = false;
|
||||
this.loadMore();
|
||||
},
|
||||
async loadMore() {
|
||||
if (this.loading || this.finished)
|
||||
return;
|
||||
this.loading = true;
|
||||
try {
|
||||
const params = { kw: this.query.kw, page: this.query.page, size: this.query.size };
|
||||
if (this.tab === "category" && this.query.categoryId)
|
||||
params.categoryId = this.query.categoryId;
|
||||
const res = await common_http.get("/api/products", params);
|
||||
const list = Array.isArray(res == null ? void 0 : res.list) ? res.list : Array.isArray(res) ? res : [];
|
||||
this.items = this.items.concat(list);
|
||||
if (list.length < this.query.size)
|
||||
this.finished = true;
|
||||
this.query.page += 1;
|
||||
} catch (e) {
|
||||
common_vendor.index.showToast({ title: "加载失败", icon: "none" });
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
openForm(id) {
|
||||
const url = "/pages/product/form" + (id ? "?id=" + id : "");
|
||||
common_vendor.index.navigateTo({ url });
|
||||
}
|
||||
}
|
||||
};
|
||||
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
|
||||
return common_vendor.e({
|
||||
a: $data.tab === "all" ? 1 : "",
|
||||
b: common_vendor.o(($event) => $options.switchTab("all")),
|
||||
c: $data.tab === "category" ? 1 : "",
|
||||
d: common_vendor.o(($event) => $options.switchTab("category")),
|
||||
e: common_vendor.o((...args) => $options.reload && $options.reload(...args)),
|
||||
f: $data.query.kw,
|
||||
g: common_vendor.o(common_vendor.m(($event) => $data.query.kw = $event.detail.value, {
|
||||
trim: true
|
||||
})),
|
||||
h: $data.tab === "category"
|
||||
}, $data.tab === "category" ? {
|
||||
i: common_vendor.t($options.categoryLabel),
|
||||
j: $options.categoryNames,
|
||||
k: common_vendor.o((...args) => $options.onPickCategory && $options.onPickCategory(...args))
|
||||
} : {}, {
|
||||
l: common_vendor.o((...args) => $options.reload && $options.reload(...args)),
|
||||
m: $data.items.length
|
||||
}, $data.items.length ? {
|
||||
n: common_vendor.f($data.items, (it, k0, i0) => {
|
||||
return common_vendor.e({
|
||||
a: it.cover
|
||||
}, it.cover ? {
|
||||
b: it.cover
|
||||
} : {}, {
|
||||
c: common_vendor.t(it.name),
|
||||
d: common_vendor.t(it.brand || "-"),
|
||||
e: common_vendor.t(it.model || ""),
|
||||
f: common_vendor.t(it.spec || ""),
|
||||
g: common_vendor.t(it.stock ?? 0),
|
||||
h: common_vendor.t((it.retailPrice ?? it.price ?? 0).toFixed(2)),
|
||||
i: it.id,
|
||||
j: common_vendor.o(($event) => $options.openForm(it.id), it.id)
|
||||
});
|
||||
})
|
||||
} : {}, {
|
||||
o: common_vendor.o((...args) => $options.loadMore && $options.loadMore(...args)),
|
||||
p: common_vendor.o(($event) => $options.openForm())
|
||||
});
|
||||
}
|
||||
const MiniProgramPage = /* @__PURE__ */ common_vendor._export_sfc(_sfc_main, [["render", _sfc_render]]);
|
||||
wx.createPage(MiniProgramPage);
|
||||
//# sourceMappingURL=../../../.sourcemap/mp-weixin/pages/product/list.js.map
|
||||
4
frontend/unpackage/dist/dev/mp-weixin/pages/product/list.json
vendored
Normal file
4
frontend/unpackage/dist/dev/mp-weixin/pages/product/list.json
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"navigationBarTitleText": "货品列表",
|
||||
"usingComponents": {}
|
||||
}
|
||||
1
frontend/unpackage/dist/dev/mp-weixin/pages/product/list.wxml
vendored
Normal file
1
frontend/unpackage/dist/dev/mp-weixin/pages/product/list.wxml
vendored
Normal file
@@ -0,0 +1 @@
|
||||
<view class="page"><view class="tabs"><view class="{{['tab', a && 'active']}}" bindtap="{{b}}">全部</view><view class="{{['tab', c && 'active']}}" bindtap="{{d}}">按类别</view></view><view class="search"><input placeholder="输入名称/条码/规格查询" bindconfirm="{{e}}" value="{{f}}" bindinput="{{g}}"/><picker wx:if="{{h}}" mode="selector" range="{{j}}" bindchange="{{k}}"><view class="picker">{{i}}</view></picker><button size="mini" bindtap="{{l}}">查询</button></view><scroll-view scroll-y class="list" bindscrolltolower="{{o}}"><block wx:if="{{m}}"><view wx:for="{{n}}" wx:for-item="it" wx:key="i" class="item" bindtap="{{it.j}}"><image wx:if="{{it.a}}" src="{{it.b}}" class="thumb" mode="aspectFill"/><view class="content"><view class="name">{{it.c}}</view><view class="meta">{{it.d}} {{it.e}} {{it.f}}</view><view class="meta">库存:{{it.g}} <text class="price">零售价:¥{{it.h}}</text></view></view></view></block><view wx:else class="empty"><text>暂无数据,点击右上角“+”新增</text></view></scroll-view><view class="fab" bindtap="{{p}}">+</view></view>
|
||||
33
frontend/unpackage/dist/dev/mp-weixin/pages/product/list.wxss
vendored
Normal file
33
frontend/unpackage/dist/dev/mp-weixin/pages/product/list.wxss
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
|
||||
.page { display:flex; flex-direction: column; height: 100vh;
|
||||
}
|
||||
.tabs { display:flex; background:#fff;
|
||||
}
|
||||
.tab { flex:1; text-align:center; padding: 20rpx 0; color:#666;
|
||||
}
|
||||
.tab.active { color:#18b566; font-weight: 600;
|
||||
}
|
||||
.search { display:flex; gap: 12rpx; padding: 16rpx; background:#fff; align-items: center;
|
||||
}
|
||||
.search input { flex:1; background:#f6f6f6; border-radius: 12rpx; padding: 12rpx;
|
||||
}
|
||||
.picker { padding: 8rpx 12rpx; background:#f0f0f0; border-radius: 10rpx; color:#666;
|
||||
}
|
||||
.list { flex:1;
|
||||
}
|
||||
.item { display:flex; padding: 20rpx; background:#fff; border-bottom: 1rpx solid #f1f1f1;
|
||||
}
|
||||
.thumb { width: 120rpx; height: 120rpx; border-radius: 12rpx; margin-right: 16rpx; background:#fafafa;
|
||||
}
|
||||
.content { flex:1;
|
||||
}
|
||||
.name { color:#333; margin-bottom: 6rpx; font-weight: 600;
|
||||
}
|
||||
.meta { color:#888; font-size: 24rpx;
|
||||
}
|
||||
.price { margin-left: 20rpx; color:#f60;
|
||||
}
|
||||
.empty { height: 60vh; display:flex; align-items:center; justify-content:center; color:#999;
|
||||
}
|
||||
.fab { position: fixed; right: 30rpx; bottom: 120rpx; width: 100rpx; height: 100rpx; background:#18b566; color:#fff; border-radius: 50rpx; text-align:center; line-height: 100rpx; font-size: 48rpx; box-shadow: 0 8rpx 20rpx rgba(0,0,0,0.15);
|
||||
}
|
||||
39
frontend/unpackage/dist/dev/mp-weixin/pages/product/settings.js
vendored
Normal file
39
frontend/unpackage/dist/dev/mp-weixin/pages/product/settings.js
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
"use strict";
|
||||
const common_http = require("../../common/http.js");
|
||||
const common_vendor = require("../../common/vendor.js");
|
||||
const _sfc_main = {
|
||||
data() {
|
||||
return { settings: { hideZeroStock: false, hidePurchasePrice: false } };
|
||||
},
|
||||
onLoad() {
|
||||
this.load();
|
||||
},
|
||||
methods: {
|
||||
async load() {
|
||||
try {
|
||||
const res = await common_http.get("/api/product-settings");
|
||||
this.settings = { hideZeroStock: !!(res == null ? void 0 : res.hideZeroStock), hidePurchasePrice: !!(res == null ? void 0 : res.hidePurchasePrice) };
|
||||
} catch (_) {
|
||||
}
|
||||
},
|
||||
async update(key, val) {
|
||||
const next = { ...this.settings, [key]: val };
|
||||
this.settings = next;
|
||||
try {
|
||||
await common_http.put("/api/product-settings", next);
|
||||
} catch (_) {
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
|
||||
return {
|
||||
a: $data.settings.hideZeroStock,
|
||||
b: common_vendor.o((e) => $options.update("hideZeroStock", e.detail.value)),
|
||||
c: $data.settings.hidePurchasePrice,
|
||||
d: common_vendor.o((e) => $options.update("hidePurchasePrice", e.detail.value))
|
||||
};
|
||||
}
|
||||
const MiniProgramPage = /* @__PURE__ */ common_vendor._export_sfc(_sfc_main, [["render", _sfc_render]]);
|
||||
wx.createPage(MiniProgramPage);
|
||||
//# sourceMappingURL=../../../.sourcemap/mp-weixin/pages/product/settings.js.map
|
||||
4
frontend/unpackage/dist/dev/mp-weixin/pages/product/settings.json
vendored
Normal file
4
frontend/unpackage/dist/dev/mp-weixin/pages/product/settings.json
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"navigationBarTitleText": "货品设置",
|
||||
"usingComponents": {}
|
||||
}
|
||||
1
frontend/unpackage/dist/dev/mp-weixin/pages/product/settings.wxml
vendored
Normal file
1
frontend/unpackage/dist/dev/mp-weixin/pages/product/settings.wxml
vendored
Normal file
@@ -0,0 +1 @@
|
||||
<view class="page"><view class="item"><text>隐藏零库存商品</text><switch checked="{{a}}" bindchange="{{b}}"/></view><view class="item"><text>隐藏进货价</text><switch checked="{{c}}" bindchange="{{d}}"/></view></view>
|
||||
5
frontend/unpackage/dist/dev/mp-weixin/pages/product/settings.wxss
vendored
Normal file
5
frontend/unpackage/dist/dev/mp-weixin/pages/product/settings.wxss
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
|
||||
.page { background:#fff;
|
||||
}
|
||||
.item { display:flex; justify-content: space-between; align-items:center; padding: 20rpx; border-bottom: 1rpx solid #f1f1f1;
|
||||
}
|
||||
62
frontend/unpackage/dist/dev/mp-weixin/pages/product/units.js
vendored
Normal file
62
frontend/unpackage/dist/dev/mp-weixin/pages/product/units.js
vendored
Normal file
@@ -0,0 +1,62 @@
|
||||
"use strict";
|
||||
const common_vendor = require("../../common/vendor.js");
|
||||
const common_http = require("../../common/http.js");
|
||||
const _sfc_main = {
|
||||
data() {
|
||||
return { name: "", list: [] };
|
||||
},
|
||||
onLoad() {
|
||||
this.reload();
|
||||
},
|
||||
methods: {
|
||||
async reload() {
|
||||
try {
|
||||
const res = await common_http.get("/api/product-units");
|
||||
this.list = Array.isArray(res == null ? void 0 : res.list) ? res.list : Array.isArray(res) ? res : [];
|
||||
} catch (_) {
|
||||
}
|
||||
},
|
||||
async create() {
|
||||
if (!this.name)
|
||||
return;
|
||||
await common_http.post("/api/product-units", { name: this.name });
|
||||
this.name = "";
|
||||
this.reload();
|
||||
},
|
||||
async update(u) {
|
||||
await common_http.put("/api/product-units/" + u.id, { name: u.name });
|
||||
common_vendor.index.showToast({ title: "已保存", icon: "success" });
|
||||
},
|
||||
async remove(u) {
|
||||
common_vendor.index.showModal({ content: "确定删除该单位?", success: async (r) => {
|
||||
if (!r.confirm)
|
||||
return;
|
||||
await common_http.del("/api/product-units/" + u.id);
|
||||
this.reload();
|
||||
} });
|
||||
}
|
||||
}
|
||||
};
|
||||
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
|
||||
return {
|
||||
a: $data.name,
|
||||
b: common_vendor.o(common_vendor.m(($event) => $data.name = $event.detail.value, {
|
||||
trim: true
|
||||
})),
|
||||
c: common_vendor.o((...args) => $options.create && $options.create(...args)),
|
||||
d: common_vendor.f($data.list, (u, k0, i0) => {
|
||||
return {
|
||||
a: u.name,
|
||||
b: common_vendor.o(common_vendor.m(($event) => u.name = $event.detail.value, {
|
||||
trim: true
|
||||
}), u.id),
|
||||
c: common_vendor.o(($event) => $options.update(u), u.id),
|
||||
d: common_vendor.o(($event) => $options.remove(u), u.id),
|
||||
e: u.id
|
||||
};
|
||||
})
|
||||
};
|
||||
}
|
||||
const MiniProgramPage = /* @__PURE__ */ common_vendor._export_sfc(_sfc_main, [["render", _sfc_render]]);
|
||||
wx.createPage(MiniProgramPage);
|
||||
//# sourceMappingURL=../../../.sourcemap/mp-weixin/pages/product/units.js.map
|
||||
4
frontend/unpackage/dist/dev/mp-weixin/pages/product/units.json
vendored
Normal file
4
frontend/unpackage/dist/dev/mp-weixin/pages/product/units.json
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"navigationBarTitleText": "单位管理",
|
||||
"usingComponents": {}
|
||||
}
|
||||
1
frontend/unpackage/dist/dev/mp-weixin/pages/product/units.wxml
vendored
Normal file
1
frontend/unpackage/dist/dev/mp-weixin/pages/product/units.wxml
vendored
Normal file
@@ -0,0 +1 @@
|
||||
<view class="page"><view class="toolbar"><input placeholder="新单位名称" value="{{a}}" bindinput="{{b}}"/><button size="mini" bindtap="{{c}}">新增</button></view><scroll-view scroll-y class="list"><view wx:for="{{d}}" wx:for-item="u" wx:key="e" class="item"><input value="{{u.a}}" bindinput="{{u.b}}"/><view class="ops"><button size="mini" bindtap="{{u.c}}">保存</button><button size="mini" type="warn" bindtap="{{u.d}}">删除</button></view></view></scroll-view></view>
|
||||
15
frontend/unpackage/dist/dev/mp-weixin/pages/product/units.wxss
vendored
Normal file
15
frontend/unpackage/dist/dev/mp-weixin/pages/product/units.wxss
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
|
||||
.page { display:flex; flex-direction: column; height: 100vh;
|
||||
}
|
||||
.toolbar { display:flex; gap: 12rpx; padding: 16rpx; background:#fff;
|
||||
}
|
||||
.toolbar input { flex:1; background:#f6f6f6; border-radius: 12rpx; padding: 12rpx;
|
||||
}
|
||||
.list { flex:1;
|
||||
}
|
||||
.item { display:flex; gap: 12rpx; align-items:center; padding: 16rpx; background:#fff; border-bottom: 1rpx solid #f1f1f1;
|
||||
}
|
||||
.item input { flex:1; background:#f7f7f7; border-radius: 10rpx; padding: 12rpx;
|
||||
}
|
||||
.ops { display:flex; gap: 10rpx;
|
||||
}
|
||||
5
frontend/unpackage/dist/dev/mp-weixin/project.private.config.json
vendored
Normal file
5
frontend/unpackage/dist/dev/mp-weixin/project.private.config.json
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"setting": {
|
||||
"urlCheck": false
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user