This commit is contained in:
2025-09-20 12:05:53 +08:00
parent bff3d0414d
commit 9b107d665a
73 changed files with 2903 additions and 140 deletions

View File

@@ -0,0 +1,65 @@
package com.example.demo.account;
import com.example.demo.common.AppDefaultsProperties;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/accounts")
public class AccountController {
private final AccountService accountService;
private final AppDefaultsProperties defaults;
public AccountController(AccountService accountService, AppDefaultsProperties defaults) {
this.accountService = accountService;
this.defaults = defaults;
}
@GetMapping
public ResponseEntity<?> list(@RequestHeader(name = "X-Shop-Id", required = false) Long shopId,
@RequestParam(name = "kw", required = false) String kw,
@RequestParam(name = "status", required = false) Integer status,
@RequestParam(name = "page", defaultValue = "1") int page,
@RequestParam(name = "size", defaultValue = "50") int size) {
Long sid = (shopId == null ? defaults.getShopId() : shopId);
return ResponseEntity.ok(accountService.list(sid, kw, status == null ? 1 : status, Math.max(0, page - 1), size));
}
@PostMapping
public ResponseEntity<?> create(@RequestHeader(name = "X-Shop-Id", required = false) Long shopId,
@RequestHeader(name = "X-User-Id", required = false) Long userId,
@RequestBody AccountDtos.CreateAccountRequest req) {
Long sid = (shopId == null ? defaults.getShopId() : shopId);
Long uid = (userId == null ? defaults.getUserId() : userId);
Long id = accountService.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 AccountDtos.CreateAccountRequest req) {
Long sid = (shopId == null ? defaults.getShopId() : shopId);
Long uid = (userId == null ? defaults.getUserId() : userId);
accountService.update(id, sid, uid, req);
return ResponseEntity.ok().build();
}
@GetMapping("/{id}/ledger")
public ResponseEntity<?> ledger(@PathVariable("id") Long id,
@RequestHeader(name = "X-Shop-Id", required = false) Long shopId,
@RequestParam(name = "startDate", required = false) String startDate,
@RequestParam(name = "endDate", required = false) String endDate,
@RequestParam(name = "kw", required = false) String kw,
@RequestParam(name = "page", defaultValue = "1") int page,
@RequestParam(name = "size", defaultValue = "20") int size) {
Long sid = (shopId == null ? defaults.getShopId() : shopId);
return ResponseEntity.ok(accountService.ledger(id, sid, kw, Math.max(0, page-1), size, startDate, endDate));
}
}

View File

@@ -0,0 +1,17 @@
package com.example.demo.account;
import java.math.BigDecimal;
public class AccountDtos {
public static class CreateAccountRequest {
public String name;
public String type; // cash, bank, alipay, wechat, other
public String bankName;
public String bankAccount;
public BigDecimal openingBalance; // 可选,创建时作为期初
public Integer status; // 1 启用 0 停用
}
}

View File

@@ -0,0 +1,143 @@
package com.example.demo.account;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Service
public class AccountService {
private final JdbcTemplate jdbcTemplate;
public AccountService(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
public Object list(Long shopId, String kw, Integer status, int page, int size) {
StringBuilder sql = new StringBuilder("SELECT id, name, type, bank_name, bank_account, balance, status FROM accounts WHERE shop_id=?");
java.util.List<Object> ps = new java.util.ArrayList<>(); ps.add(shopId);
if (status != null) { sql.append(" AND status=?"); ps.add(status); }
if (kw != null && !kw.isBlank()) { sql.append(" AND (name LIKE ? OR bank_name LIKE ? OR bank_account LIKE ?)"); ps.add('%'+kw+'%'); ps.add('%'+kw+'%'); ps.add('%'+kw+'%'); }
sql.append(" ORDER BY id DESC LIMIT ? OFFSET ?"); ps.add(size); ps.add(page * size);
List<Map<String,Object>> list = jdbcTemplate.queryForList(sql.toString(), ps.toArray());
Map<String,Object> body = new HashMap<>();
body.put("list", list);
return body;
}
@Transactional
public Long create(Long shopId, Long userId, AccountDtos.CreateAccountRequest req) {
if (req == null || req.name == null || req.name.isBlank()) throw new IllegalArgumentException("账户名称必填");
String type = (req.type == null || req.type.isBlank()) ? "cash" : req.type.toLowerCase();
int status = req.status == null ? 1 : req.status;
jdbcTemplate.update("INSERT INTO accounts (shop_id,user_id,name,type,bank_name,bank_account,balance,status,created_at,updated_at) VALUES (?,?,?,?,?,?,0,?,NOW(),NOW())",
shopId, userId, req.name, type, req.bankName, req.bankAccount, status);
Long id = jdbcTemplate.queryForObject("SELECT id FROM accounts WHERE shop_id=? AND name=? ORDER BY id DESC LIMIT 1", Long.class, shopId, req.name);
BigDecimal opening = req.openingBalance == null ? BigDecimal.ZERO : req.openingBalance.setScale(2, java.math.RoundingMode.HALF_UP);
if (opening.compareTo(BigDecimal.ZERO) != 0) {
java.sql.Timestamp now = new java.sql.Timestamp(System.currentTimeMillis());
// other_transactions
String otType = opening.compareTo(BigDecimal.ZERO) > 0 ? "income" : "expense";
BigDecimal amt = opening.abs();
jdbcTemplate.update("INSERT INTO other_transactions (shop_id,user_id,type,category,counterparty_type,counterparty_id,account_id,amount,tx_time,remark,created_at,updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,NOW(),NOW())",
shopId, userId, otType, "account_operation", null, null, id, amt, now, "期初余额");
// payments
String direction = opening.compareTo(BigDecimal.ZERO) > 0 ? "in" : "out";
jdbcTemplate.update("INSERT INTO payments (shop_id,user_id,biz_type,biz_id,account_id,direction,amount,pay_time,remark,created_at) VALUES (?,?,?,?,?,?,?,?,?,NOW())",
shopId, userId, "other", null, id, direction, amt, now, "期初余额");
// update balance
BigDecimal delta = opening;
jdbcTemplate.update("UPDATE accounts SET balance = balance + ?, updated_at=NOW() WHERE id=? AND shop_id=?", delta, id, shopId);
}
return id;
}
@Transactional
public void update(Long id, Long shopId, Long userId, AccountDtos.CreateAccountRequest req) {
StringBuilder sql = new StringBuilder("UPDATE accounts SET updated_at=NOW()");
java.util.List<Object> ps = new java.util.ArrayList<>();
if (req.name != null) { sql.append(", name=?"); ps.add(req.name); }
if (req.type != null) { sql.append(", type=?"); ps.add(req.type.toLowerCase()); }
if (req.bankName != null) { sql.append(", bank_name=?"); ps.add(req.bankName); }
if (req.bankAccount != null) { sql.append(", bank_account=?"); ps.add(req.bankAccount); }
if (req.status != null) { sql.append(", status=?"); ps.add(req.status); }
sql.append(" WHERE id=? AND shop_id=?"); ps.add(id); ps.add(shopId);
jdbcTemplate.update(sql.toString(), ps.toArray());
}
public Map<String,Object> ledger(Long accountId, Long shopId, String kw, int page, int size, String startDate, String endDate) {
// 汇总
String baseCond = " shop_id=? AND account_id=?";
java.util.List<Object> basePs = new java.util.ArrayList<>(); basePs.add(shopId); basePs.add(accountId);
java.util.function.BiFunction<String, java.util.List<Object>, java.math.BigDecimal> sum = (sql, ps) -> {
java.math.BigDecimal v = jdbcTemplate.queryForObject(sql, java.math.BigDecimal.class, ps.toArray());
return v == null ? java.math.BigDecimal.ZERO : v;
};
String dateStart = (startDate == null || startDate.isBlank()) ? null : startDate;
String dateEnd = (endDate == null || endDate.isBlank()) ? null : endDate;
// opening = 截止开始日期前净额
String payOpenSql = "SELECT COALESCE(SUM(CASE WHEN direction='in' THEN amount ELSE -amount END),0) FROM payments WHERE" + baseCond + (dateStart==null?"":" AND pay_time<?");
java.util.List<Object> payOpenPs = new java.util.ArrayList<>(basePs);
if (dateStart!=null) payOpenPs.add(java.sql.Timestamp.valueOf(dateStart + " 00:00:00"));
String otOpenSql = "SELECT COALESCE(SUM(CASE WHEN type='income' THEN amount ELSE -amount END),0) FROM other_transactions WHERE" + baseCond + (dateStart==null?"":" AND tx_time<?");
java.util.List<Object> otOpenPs = new java.util.ArrayList<>(basePs);
if (dateStart!=null) otOpenPs.add(java.sql.Timestamp.valueOf(dateStart + " 00:00:00"));
java.math.BigDecimal opening = sum.apply(payOpenSql, payOpenPs).add(sum.apply(otOpenSql, otOpenPs));
// 区间收入/支出(含两表)
String payRangeSql = "SELECT COALESCE(SUM(CASE WHEN direction='in' THEN amount ELSE 0 END),0), COALESCE(SUM(CASE WHEN direction='out' THEN amount ELSE 0 END),0) FROM payments WHERE" + baseCond +
(dateStart==null?"":" AND pay_time>=?") + (dateEnd==null?"":" AND pay_time<=?");
java.util.List<Object> payRangePs = new java.util.ArrayList<>(basePs);
if (dateStart!=null) payRangePs.add(java.sql.Timestamp.valueOf(dateStart + " 00:00:00"));
if (dateEnd!=null) payRangePs.add(java.sql.Timestamp.valueOf(dateEnd + " 23:59:59"));
java.util.Map<String, Object> pr = jdbcTemplate.queryForMap(payRangeSql, payRangePs.toArray());
java.math.BigDecimal payIn = (java.math.BigDecimal) pr.values().toArray()[0];
java.math.BigDecimal payOut = (java.math.BigDecimal) pr.values().toArray()[1];
String otRangeSql = "SELECT COALESCE(SUM(CASE WHEN type='income' THEN amount ELSE 0 END),0), COALESCE(SUM(CASE WHEN type='expense' THEN amount ELSE 0 END),0) FROM other_transactions WHERE" + baseCond +
(dateStart==null?"":" AND tx_time>=?") + (dateEnd==null?"":" AND tx_time<=?");
java.util.List<Object> otRangePs = new java.util.ArrayList<>(basePs);
if (dateStart!=null) otRangePs.add(java.sql.Timestamp.valueOf(dateStart + " 00:00:00"));
if (dateEnd!=null) otRangePs.add(java.sql.Timestamp.valueOf(dateEnd + " 23:59:59"));
java.util.Map<String, Object> or = jdbcTemplate.queryForMap(otRangeSql, otRangePs.toArray());
java.math.BigDecimal otIn = (java.math.BigDecimal) or.values().toArray()[0];
java.math.BigDecimal otOut = (java.math.BigDecimal) or.values().toArray()[1];
java.math.BigDecimal income = payIn.add(otIn);
java.math.BigDecimal expense = payOut.add(otOut);
java.math.BigDecimal ending = opening.add(income).subtract(expense);
// 明细列表(合并两表,按时间倒序)
String listSql = "SELECT id, biz_type AS src, pay_time AS tx_time, direction, amount, remark, biz_id, NULL AS category FROM payments WHERE" + baseCond +
(dateStart==null?"":" AND pay_time>=?") + (dateEnd==null?"":" AND pay_time<=?") +
" UNION ALL " +
"SELECT id, 'other' AS src, tx_time, CASE WHEN type='income' THEN 'in' ELSE 'out' END AS direction, amount, remark, NULL AS biz_id, category FROM other_transactions WHERE" + baseCond +
(dateStart==null?"":" AND tx_time>=?") + (dateEnd==null?"":" AND tx_time<=?") +
" ORDER BY tx_time DESC LIMIT ? OFFSET ?";
java.util.List<Object> lp = new java.util.ArrayList<>(basePs);
if (dateStart!=null) { lp.add(java.sql.Timestamp.valueOf(dateStart + " 00:00:00")); }
if (dateEnd!=null) { lp.add(java.sql.Timestamp.valueOf(dateEnd + " 23:59:59")); }
if (dateStart!=null) { lp.add(java.sql.Timestamp.valueOf(dateStart + " 00:00:00")); }
if (dateEnd!=null) { lp.add(java.sql.Timestamp.valueOf(dateEnd + " 23:59:59")); }
lp.add(size); lp.add(page * size);
List<Map<String,Object>> list = jdbcTemplate.queryForList(listSql, lp.toArray());
Map<String,Object> resp = new HashMap<>();
resp.put("opening", opening);
resp.put("income", income);
resp.put("expense", expense);
resp.put("ending", ending);
resp.put("list", list);
return resp;
}
}

View File

@@ -0,0 +1,25 @@
package com.example.demo.common;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Component
@ConfigurationProperties(prefix = "app.account.defaults")
public class AccountDefaultsProperties {
private String cashName = "现金";
private String bankName = "银行存款";
private String wechatName = "微信";
private String alipayName = "支付宝";
public String getCashName() { return cashName; }
public void setCashName(String cashName) { this.cashName = cashName; }
public String getBankName() { return bankName; }
public void setBankName(String bankName) { this.bankName = bankName; }
public String getWechatName() { return wechatName; }
public void setWechatName(String wechatName) { this.wechatName = wechatName; }
public String getAlipayName() { return alipayName; }
public void setAlipayName(String alipayName) { this.alipayName = alipayName; }
}

View File

@@ -10,10 +10,25 @@ public class AppDefaultsProperties {
private Long shopId = 1L;
private Long userId = 2L;
// 默认账户名称(可配置,避免硬编码)
private String accountCashName = "现金";
private String accountBankName = "银行存款";
private String accountWechatName = "微信";
private String accountAlipayName = "支付宝";
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 getAccountCashName() { return accountCashName; }
public void setAccountCashName(String accountCashName) { this.accountCashName = accountCashName; }
public String getAccountBankName() { return accountBankName; }
public void setAccountBankName(String accountBankName) { this.accountBankName = accountBankName; }
public String getAccountWechatName() { return accountWechatName; }
public void setAccountWechatName(String accountWechatName) { this.accountWechatName = accountWechatName; }
public String getAccountAlipayName() { return accountAlipayName; }
public void setAccountAlipayName(String accountAlipayName) { this.accountAlipayName = accountAlipayName; }
}

View File

@@ -0,0 +1,29 @@
package com.example.demo.common;
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.Map;
@RestController
public class FinanceController {
private final FinanceService financeService;
private final AppDefaultsProperties defaults;
public FinanceController(FinanceService financeService, AppDefaultsProperties defaults) {
this.financeService = financeService;
this.defaults = defaults;
}
@GetMapping("/api/finance/categories")
public ResponseEntity<?> listCategories(@RequestHeader(name = "X-Shop-Id", required = false) Long shopId) {
Long sid = (shopId == null ? defaults.getShopId() : shopId);
Map<String, Object> body = financeService.getCategories(sid);
return ResponseEntity.ok(body);
}
}

View File

@@ -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.finance")
public class FinanceDefaultsProperties {
// 形如 key:label, key:label 用逗号分隔
private String incomeCategories;
private String expenseCategories;
public String getIncomeCategories() { return incomeCategories; }
public void setIncomeCategories(String incomeCategories) { this.incomeCategories = incomeCategories; }
public String getExpenseCategories() { return expenseCategories; }
public void setExpenseCategories(String expenseCategories) { this.expenseCategories = expenseCategories; }
}

View File

@@ -0,0 +1,126 @@
package com.example.demo.common;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.stereotype.Service;
import java.util.*;
import java.util.stream.Collectors;
@Service
public class FinanceService {
private static final String PARAM_KEY = "finance.categories";
private final SystemParameterRepository systemParameterRepository;
private final FinanceDefaultsProperties financeDefaultsProperties;
private final javax.sql.DataSource dataSource;
private final AppDefaultsProperties appDefaults;
private final ObjectMapper objectMapper = new ObjectMapper();
public FinanceService(SystemParameterRepository systemParameterRepository, FinanceDefaultsProperties financeDefaultsProperties, javax.sql.DataSource dataSource, AppDefaultsProperties appDefaults) {
this.systemParameterRepository = systemParameterRepository;
this.financeDefaultsProperties = financeDefaultsProperties;
this.dataSource = dataSource;
this.appDefaults = appDefaults;
}
public Map<String, Object> getCategories(Long shopId) {
Map<String, Object> body = new HashMap<>();
// 0) 优先从 finance_categories 表读取(避免中文乱码/统一排序)
List<Map<String, String>> income = queryCategoriesFromTable(shopId, "income");
List<Map<String, String>> expense = queryCategoriesFromTable(shopId, "expense");
// 1) 回落读取 system_parameters
try {
if (income == null || income.isEmpty() || expense == null || expense.isEmpty()) {
Optional<SystemParameter> opt = systemParameterRepository.findByShopIdAndKey(shopId, PARAM_KEY);
if (opt.isPresent()) {
String json = opt.get().getValue();
if (json != null && !json.isBlank()) {
JsonNode root = objectMapper.readTree(json);
if (income == null || income.isEmpty()) {
JsonNode incNode = root.get("income");
if (incNode != null && incNode.isArray()) {
income = objectMapper.convertValue(incNode, new TypeReference<List<Map<String,String>>>(){});
}
}
if (expense == null || expense.isEmpty()) {
JsonNode expNode = root.get("expense");
if (expNode != null && expNode.isArray()) {
expense = objectMapper.convertValue(expNode, new TypeReference<List<Map<String,String>>>(){});
}
}
}
}
}
} catch (Exception ignored) {
// 忽略异常,回落至默认配置
}
// 2) 回落:应用配置 app.finance.*
if (income == null || income.isEmpty()) {
income = parsePairs(financeDefaultsProperties.getIncomeCategories());
}
if (expense == null || expense.isEmpty()) {
expense = parsePairs(financeDefaultsProperties.getExpenseCategories());
}
body.put("incomeCategories", income);
body.put("expenseCategories", expense);
return body;
}
private List<Map<String, String>> parsePairs(String pairs) {
if (pairs == null || pairs.isBlank()) return Collections.emptyList();
return Arrays.stream(pairs.split(","))
.map(String::trim)
.filter(s -> !s.isEmpty())
.map(s -> {
int idx = s.indexOf(":");
String key = idx > 0 ? s.substring(0, idx).trim() : s.trim();
String label = idx > 0 ? s.substring(idx + 1).trim() : key;
Map<String, String> m = new HashMap<>();
m.put("key", key);
m.put("label", label);
return m;
})
.collect(Collectors.toList());
}
private List<Map<String, String>> queryCategoriesFromTable(Long shopId, String type) {
Long fallbackShopId = appDefaults == null ? 1L : (appDefaults.getShopId() == null ? 1L : appDefaults.getShopId());
try (java.sql.Connection c = dataSource.getConnection();
java.sql.PreparedStatement ps = c.prepareStatement(
"SELECT shop_id, `key`, label FROM finance_categories WHERE shop_id IN (?, ?) AND type=? AND status=1 " +
"ORDER BY CASE WHEN shop_id=? THEN 0 ELSE 1 END, sort_order, id")) {
ps.setLong(1, shopId);
ps.setLong(2, fallbackShopId);
ps.setString(3, type);
ps.setLong(4, shopId);
try (java.sql.ResultSet rs = ps.executeQuery()) {
java.util.Map<String,String> firstByKey = new java.util.LinkedHashMap<>();
while (rs.next()) {
String key = rs.getString(2);
String label = rs.getString(3);
if (!firstByKey.containsKey(key)) {
firstByKey.put(key, label);
}
}
java.util.List<java.util.Map<String,String>> list = new java.util.ArrayList<>();
for (java.util.Map.Entry<String,String> e : firstByKey.entrySet()) {
java.util.Map<String,String> m = new java.util.HashMap<>();
m.put("key", e.getKey());
m.put("label", e.getValue());
list.add(m);
}
return list;
}
} catch (Exception ignored) {
return java.util.Collections.emptyList();
}
}
}

View File

@@ -0,0 +1,50 @@
package com.example.demo.common;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.lang.NonNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
@Component
public class RequestLoggingFilter extends OncePerRequestFilter {
private static final Logger log = LoggerFactory.getLogger(RequestLoggingFilter.class);
@Override
protected void doFilterInternal(@NonNull HttpServletRequest request,
@NonNull HttpServletResponse response,
@NonNull FilterChain filterChain) throws ServletException, IOException {
long start = System.currentTimeMillis();
String method = request.getMethod();
String uri = request.getRequestURI();
String query = request.getQueryString();
String shopId = request.getHeader("X-Shop-Id");
String userId = request.getHeader("X-User-Id");
try {
filterChain.doFilter(request, response);
} finally {
long cost = System.currentTimeMillis() - start;
int status = response.getStatus();
if (log.isDebugEnabled()) {
log.debug("{} {}{} | status={} cost={}ms | shopId={} userId={}",
method,
uri,
(query == null ? "" : ("?" + query)),
status,
cost,
(shopId == null ? "" : shopId),
(userId == null ? "" : userId));
}
}
}
}

View File

@@ -0,0 +1,48 @@
package com.example.demo.common;
import jakarta.persistence.*;
import java.time.LocalDateTime;
@Entity
@Table(name = "system_parameters")
public class SystemParameter {
@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 = "`key`", nullable = false, length = 64)
private String key;
@Column(name = "value", nullable = false, columnDefinition = "JSON")
private String value;
@Column(name = "created_at", nullable = false)
private LocalDateTime createdAt;
@Column(name = "updated_at", nullable = false)
private LocalDateTime updatedAt;
public Long getId() { return id; }
public void setId(Long id) { this.id = 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 getKey() { return key; }
public void setKey(String key) { this.key = key; }
public String getValue() { return value; }
public void setValue(String value) { this.value = value; }
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; }
}

View File

@@ -0,0 +1,11 @@
package com.example.demo.common;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface SystemParameterRepository extends JpaRepository<SystemParameter, Long> {
Optional<SystemParameter> findByShopIdAndKey(Long shopId, String key);
}

View File

@@ -60,6 +60,13 @@ public class OrderController {
return ResponseEntity.ok(orderService.list(sid, biz, type, kw, Math.max(0, page-1), size, startDate, endDate));
}
@GetMapping("/orders/{id}")
public ResponseEntity<?> getOrderDetail(@RequestHeader(name = "X-Shop-Id", required = false) Long shopId,
@PathVariable("id") Long id) {
Long sid = (shopId == null ? defaults.getShopId() : shopId);
return ResponseEntity.ok(orderService.getSalesOrderDetail(sid, id));
}
// 兼容前端直接调用 /api/purchase-orders
@GetMapping("/purchase-orders")
public ResponseEntity<?> purchaseOrders(@RequestHeader(name = "X-Shop-Id", required = false) Long shopId,
@@ -74,6 +81,13 @@ public class OrderController {
return ResponseEntity.ok(orderService.list(sid, "purchase", type, kw, Math.max(0, page-1), size, startDate, endDate));
}
@GetMapping("/purchase-orders/{id}")
public ResponseEntity<?> getPurchaseOrderDetail(@RequestHeader(name = "X-Shop-Id", required = false) Long shopId,
@PathVariable("id") Long id) {
Long sid = (shopId == null ? defaults.getShopId() : shopId);
return ResponseEntity.ok(orderService.getPurchaseOrderDetail(sid, id));
}
@GetMapping("/payments")
public ResponseEntity<?> listPayments(@RequestHeader(name = "X-Shop-Id", required = false) Long shopId,
@RequestParam(name = "direction", required = false) String direction,
@@ -101,6 +115,15 @@ public class OrderController {
return ResponseEntity.ok(orderService.listOtherTransactions(sid, type, accountId, kw, Math.max(0, page-1), size, startDate, endDate));
}
@PostMapping("/other-transactions")
public ResponseEntity<?> createOtherTransaction(@RequestHeader(name = "X-Shop-Id", required = false) Long shopId,
@RequestHeader(name = "X-User-Id", required = false) Long userId,
@RequestBody OrderDtos.CreateOtherTransactionRequest req) {
Long sid = (shopId == null ? defaults.getShopId() : shopId);
Long uid = (userId == null ? defaults.getUserId() : userId);
return ResponseEntity.ok(orderService.createOtherTransaction(sid, uid, req));
}
@GetMapping("/inventories/logs")
public ResponseEntity<?> listInventoryLogs(@RequestHeader(name = "X-Shop-Id", required = false) Long shopId,
@RequestParam(name = "productId", required = false) Long productId,

View File

@@ -1,5 +1,6 @@
package com.example.demo.order;
import com.example.demo.common.AccountDefaultsProperties;
import com.example.demo.order.dto.OrderDtos;
import com.example.demo.product.entity.Inventory;
import com.example.demo.product.repo.InventoryRepository;
@@ -18,14 +19,15 @@ import java.util.List;
public class OrderService {
private final InventoryRepository inventoryRepository;
private final AccountDefaultsProperties accountDefaults;
private final JdbcTemplate jdbcTemplate;
public OrderService(InventoryRepository inventoryRepository,
JdbcTemplate jdbcTemplate) {
JdbcTemplate jdbcTemplate,
AccountDefaultsProperties accountDefaults) {
this.inventoryRepository = inventoryRepository;
this.jdbcTemplate = jdbcTemplate;
this.accountDefaults = accountDefaults;
}
@Transactional
@@ -163,6 +165,10 @@ public class OrderService {
if (req == null) return new OrderDtos.CreatePaymentsResponse(ids);
String direction = "sale".equals(bizType) ? "in" : "out";
for (OrderDtos.PaymentItem p : req) {
// 收/付款必须绑定订单(资金类不在此接口中处理)
if (("sale".equals(bizType) || "purchase".equals(bizType)) && p.orderId == null) {
throw new IllegalArgumentException("收/付款必须绑定订单");
}
Long accountId = resolveAccountId(shopId, userId, p.method);
KeyHolder kh = new GeneratedKeyHolder();
String sql = "INSERT INTO payments (shop_id,user_id,biz_type,biz_id,account_id,direction,amount,pay_time,remark,created_at) " +
@@ -247,39 +253,56 @@ public class OrderService {
}
public java.util.Map<String,Object> list(Long shopId, String biz, String type, String kw, int page, int size, String startDate, String endDate) {
String headTable;
if ("sale".equals(biz)) {
headTable = ("sale.return".equals(type) ? "sales_return_orders" : "sales_orders");
} else if ("purchase".equals(biz)) {
headTable = ("purchase.return".equals(type) ? "purchase_orders" : "purchase_orders");
} else {
// 若未传,默认销售出货
headTable = "sales_orders";
}
StringBuilder sql = new StringBuilder("SELECT id, order_no, order_time, amount FROM " + headTable + " WHERE shop_id=?");
StringBuilder sql = new StringBuilder();
java.util.List<Object> ps = new java.util.ArrayList<>();
ps.add(shopId);
if ("purchase".equals(biz) && "purchase.return".equals(type)) {
sql.append(" AND status='returned'");
if ("purchase".equals(biz)) {
// 进货单(含退货按状态过滤),返回驼峰并带供应商名称
sql.append("SELECT po.id, po.order_no AS orderNo, po.order_time AS orderTime, po.amount, s.name AS supplierName,\n")
.append("CASE WHEN po.status='returned' THEN 'purchase.return' ELSE 'purchase.in' END AS docType\n")
.append("FROM purchase_orders po\n")
.append("LEFT JOIN suppliers s ON s.id = po.supplier_id\n")
.append("WHERE po.shop_id=?");
if ("purchase.return".equals(type)) {
sql.append(" AND po.status='returned'");
}
if (kw != null && !kw.isBlank()) {
sql.append(" AND (order_no LIKE ?)");
ps.add('%' + kw + '%');
if (kw != null && !kw.isBlank()) { sql.append(" AND (po.order_no LIKE ?)"); ps.add('%' + kw + '%'); }
if (startDate != null && !startDate.isBlank()) { sql.append(" AND po.order_time>=?"); ps.add(java.sql.Timestamp.valueOf(startDate + " 00:00:00")); }
if (endDate != null && !endDate.isBlank()) { sql.append(" AND po.order_time<=?"); ps.add(java.sql.Timestamp.valueOf(endDate + " 23:59:59")); }
sql.append(" ORDER BY po.order_time DESC LIMIT ? OFFSET ?");
} else { // 默认销售
// 销售单,返回驼峰并带客户名称
sql.append("SELECT so.id, so.order_no AS orderNo, so.order_time AS orderTime, so.amount, c.name AS customerName, 'sale.out' AS docType\n")
.append("FROM sales_orders so\n")
.append("LEFT JOIN customers c ON c.id = so.customer_id\n")
.append("WHERE so.shop_id=?");
if (kw != null && !kw.isBlank()) { sql.append(" AND (so.order_no LIKE ?)"); ps.add('%' + kw + '%'); }
if (startDate != null && !startDate.isBlank()) { sql.append(" AND so.order_time>=?"); ps.add(java.sql.Timestamp.valueOf(startDate + " 00:00:00")); }
if (endDate != null && !endDate.isBlank()) { sql.append(" AND so.order_time<=?"); ps.add(java.sql.Timestamp.valueOf(endDate + " 23:59:59")); }
sql.append(" ORDER BY so.order_time DESC LIMIT ? OFFSET ?");
}
if (startDate != null && !startDate.isBlank()) { sql.append(" AND order_time>=?"); ps.add(java.sql.Timestamp.valueOf(startDate + " 00:00:00")); }
if (endDate != null && !endDate.isBlank()) { sql.append(" AND order_time<=?"); ps.add(java.sql.Timestamp.valueOf(endDate + " 23:59:59")); }
sql.append(" ORDER BY order_time DESC LIMIT ? OFFSET ?");
ps.add(size);
ps.add(page * size);
java.util.List<java.util.Map<String,Object>> list = jdbcTemplate.queryForList(sql.toString(), ps.toArray());
// 汇总
StringBuilder sumSql = new StringBuilder("SELECT COALESCE(SUM(amount),0) FROM " + headTable + " WHERE shop_id=?");
StringBuilder sumSql;
java.util.List<Object> sumPs = new java.util.ArrayList<>();
sumPs.add(shopId);
if ("purchase".equals(biz) && "purchase.return".equals(type)) { sumSql.append(" AND status='returned'"); }
if ("purchase".equals(biz)) {
sumSql = new StringBuilder("SELECT COALESCE(SUM(amount),0) FROM purchase_orders WHERE shop_id=?");
if ("purchase.return".equals(type)) { sumSql.append(" AND status='returned'"); }
if (kw != null && !kw.isBlank()) { sumSql.append(" AND (order_no LIKE ?)"); sumPs.add('%' + kw + '%'); }
if (startDate != null && !startDate.isBlank()) { sumSql.append(" AND order_time>=?"); sumPs.add(java.sql.Timestamp.valueOf(startDate + " 00:00:00")); }
if (endDate != null && !endDate.isBlank()) { sumSql.append(" AND order_time<=?"); sumPs.add(java.sql.Timestamp.valueOf(endDate + " 23:59:59")); }
} else {
sumSql = new StringBuilder("SELECT COALESCE(SUM(amount),0) FROM sales_orders WHERE shop_id=?");
if (kw != null && !kw.isBlank()) { sumSql.append(" AND (order_no LIKE ?)"); sumPs.add('%' + kw + '%'); }
if (startDate != null && !startDate.isBlank()) { sumSql.append(" AND order_time>=?"); sumPs.add(java.sql.Timestamp.valueOf(startDate + " 00:00:00")); }
if (endDate != null && !endDate.isBlank()) { sumSql.append(" AND order_time<=?"); sumPs.add(java.sql.Timestamp.valueOf(endDate + " 23:59:59")); }
}
java.math.BigDecimal total = jdbcTemplate.queryForObject(sumSql.toString(), java.math.BigDecimal.class, sumPs.toArray());
java.util.Map<String,Object> resp = new java.util.HashMap<>();
resp.put("list", list);
@@ -288,53 +311,132 @@ public class OrderService {
}
public java.util.Map<String,Object> listPayments(Long shopId, String direction, String bizType, Long accountId, String kw, int page, int size, String startDate, String endDate) {
StringBuilder sql = new StringBuilder("SELECT id, biz_type, account_id, direction, amount, pay_time FROM payments WHERE shop_id=?");
StringBuilder sql = new StringBuilder("SELECT p.id, p.biz_type AS bizType, p.account_id, a.name AS accountName, p.direction, p.amount, p.pay_time AS orderTime,\n" +
"CASE \n" +
" WHEN p.biz_type='sale' AND p.direction='in' THEN 'sale.collect' \n" +
" WHEN p.biz_type='purchase' AND p.direction='out' THEN 'purchase.pay' \n" +
" WHEN p.biz_type='other' AND p.direction='in' THEN 'other.income' \n" +
" WHEN p.biz_type='other' AND p.direction='out' THEN 'other.expense' \n" +
" ELSE CONCAT(p.biz_type, '.', p.direction) END AS docType\n" +
"FROM payments p LEFT JOIN accounts a ON a.id=p.account_id WHERE p.shop_id=?");
java.util.List<Object> ps = new java.util.ArrayList<>();
ps.add(shopId);
if (direction != null && !direction.isBlank()) { sql.append(" AND direction=?"); ps.add(direction); }
if (bizType != null && !bizType.isBlank()) { sql.append(" AND biz_type=?"); ps.add(bizType); }
if (accountId != null) { sql.append(" AND account_id=?"); ps.add(accountId); }
if (kw != null && !kw.isBlank()) { sql.append(" AND (CAST(id AS CHAR) LIKE ?)"); ps.add('%'+kw+'%'); }
if (startDate != null && !startDate.isBlank()) { sql.append(" AND pay_time>=?"); ps.add(java.sql.Timestamp.valueOf(startDate + " 00:00:00")); }
if (endDate != null && !endDate.isBlank()) { sql.append(" AND pay_time<=?"); ps.add(java.sql.Timestamp.valueOf(endDate + " 23:59:59")); }
sql.append(" ORDER BY pay_time DESC LIMIT ? OFFSET ?");
if (direction != null && !direction.isBlank()) { sql.append(" AND p.direction=?"); ps.add(direction); }
if (bizType != null && !bizType.isBlank()) { sql.append(" AND p.biz_type=?"); ps.add(bizType); }
if (accountId != null) { sql.append(" AND p.account_id=?"); ps.add(accountId); }
if (kw != null && !kw.isBlank()) { sql.append(" AND (CAST(p.id AS CHAR) LIKE ?)"); ps.add('%'+kw+'%'); }
if (startDate != null && !startDate.isBlank()) { sql.append(" AND p.pay_time>=?"); ps.add(java.sql.Timestamp.valueOf(startDate + " 00:00:00")); }
if (endDate != null && !endDate.isBlank()) { sql.append(" AND p.pay_time<=?"); ps.add(java.sql.Timestamp.valueOf(endDate + " 23:59:59")); }
sql.append(" ORDER BY p.pay_time DESC LIMIT ? OFFSET ?");
ps.add(size); ps.add(page * size);
java.util.List<java.util.Map<String,Object>> list = jdbcTemplate.queryForList(sql.toString(), ps.toArray());
StringBuilder sumSql = new StringBuilder("SELECT COALESCE(SUM(amount),0) FROM payments WHERE shop_id=?");
StringBuilder sumSql = new StringBuilder("SELECT COALESCE(SUM(p.amount),0) FROM payments p WHERE p.shop_id=?");
java.util.List<Object> sumPs = new java.util.ArrayList<>(); sumPs.add(shopId);
if (direction != null && !direction.isBlank()) { sumSql.append(" AND direction=?"); sumPs.add(direction); }
if (bizType != null && !bizType.isBlank()) { sumSql.append(" AND biz_type=?"); sumPs.add(bizType); }
if (accountId != null) { sumSql.append(" AND account_id=?"); sumPs.add(accountId); }
if (kw != null && !kw.isBlank()) { sumSql.append(" AND (CAST(id AS CHAR) LIKE ?)"); sumPs.add('%'+kw+'%'); }
if (startDate != null && !startDate.isBlank()) { sumSql.append(" AND pay_time>=?"); sumPs.add(java.sql.Timestamp.valueOf(startDate + " 00:00:00")); }
if (endDate != null && !endDate.isBlank()) { sumSql.append(" AND pay_time<=?"); sumPs.add(java.sql.Timestamp.valueOf(endDate + " 23:59:59")); }
if (direction != null && !direction.isBlank()) { sumSql.append(" AND p.direction=?"); sumPs.add(direction); }
if (bizType != null && !bizType.isBlank()) { sumSql.append(" AND p.biz_type=?"); sumPs.add(bizType); }
if (accountId != null) { sumSql.append(" AND p.account_id=?"); sumPs.add(accountId); }
if (kw != null && !kw.isBlank()) { sumSql.append(" AND (CAST(p.id AS CHAR) LIKE ?)"); sumPs.add('%'+kw+'%'); }
if (startDate != null && !startDate.isBlank()) { sumSql.append(" AND p.pay_time>=?"); sumPs.add(java.sql.Timestamp.valueOf(startDate + " 00:00:00")); }
if (endDate != null && !endDate.isBlank()) { sumSql.append(" AND p.pay_time<=?"); sumPs.add(java.sql.Timestamp.valueOf(endDate + " 23:59:59")); }
java.math.BigDecimal total = jdbcTemplate.queryForObject(sumSql.toString(), java.math.BigDecimal.class, sumPs.toArray());
java.util.Map<String,Object> resp = new java.util.HashMap<>(); resp.put("list", list); resp.put("totalAmount", total == null ? java.math.BigDecimal.ZERO : total); return resp;
}
public java.util.Map<String,Object> listOtherTransactions(Long shopId, String type, Long accountId, String kw, int page, int size, String startDate, String endDate) {
StringBuilder sql = new StringBuilder("SELECT id, `type`, account_id, amount, tx_time, remark FROM other_transactions WHERE shop_id=?");
StringBuilder sql = new StringBuilder("SELECT ot.id, ot.`type`, CONCAT('other.', ot.`type`) AS docType, ot.account_id, a.name AS accountName, ot.amount, ot.tx_time AS txTime, ot.remark FROM other_transactions ot LEFT JOIN accounts a ON a.id=ot.account_id WHERE ot.shop_id=?");
java.util.List<Object> ps = new java.util.ArrayList<>(); ps.add(shopId);
if (type != null && !type.isBlank()) { sql.append(" AND `type`=?"); ps.add(type); }
if (accountId != null) { sql.append(" AND account_id=?"); ps.add(accountId); }
if (kw != null && !kw.isBlank()) { sql.append(" AND (remark LIKE ? OR category LIKE ?)"); ps.add('%'+kw+'%'); ps.add('%'+kw+'%'); }
if (startDate != null && !startDate.isBlank()) { sql.append(" AND tx_time>=?"); ps.add(java.sql.Timestamp.valueOf(startDate + " 00:00:00")); }
if (endDate != null && !endDate.isBlank()) { sql.append(" AND tx_time<=?"); ps.add(java.sql.Timestamp.valueOf(endDate + " 23:59:59")); }
sql.append(" ORDER BY tx_time DESC LIMIT ? OFFSET ?"); ps.add(size); ps.add(page * size);
if (type != null && !type.isBlank()) { sql.append(" AND ot.`type`=?"); ps.add(type); }
if (accountId != null) { sql.append(" AND ot.account_id=?"); ps.add(accountId); }
if (kw != null && !kw.isBlank()) { sql.append(" AND (ot.remark LIKE ? OR ot.category LIKE ?)"); ps.add('%'+kw+'%'); ps.add('%'+kw+'%'); }
if (startDate != null && !startDate.isBlank()) { sql.append(" AND ot.tx_time>=?"); ps.add(java.sql.Timestamp.valueOf(startDate + " 00:00:00")); }
if (endDate != null && !endDate.isBlank()) { sql.append(" AND ot.tx_time<=?"); ps.add(java.sql.Timestamp.valueOf(endDate + " 23:59:59")); }
sql.append(" ORDER BY ot.tx_time DESC LIMIT ? OFFSET ?"); ps.add(size); ps.add(page * size);
java.util.List<java.util.Map<String,Object>> list = jdbcTemplate.queryForList(sql.toString(), ps.toArray());
StringBuilder sumSql = new StringBuilder("SELECT COALESCE(SUM(amount),0) FROM other_transactions WHERE shop_id=?");
StringBuilder sumSql = new StringBuilder("SELECT COALESCE(SUM(ot.amount),0) FROM other_transactions ot WHERE ot.shop_id=?");
java.util.List<Object> sumPs = new java.util.ArrayList<>(); sumPs.add(shopId);
if (type != null && !type.isBlank()) { sumSql.append(" AND `type`=?"); sumPs.add(type); }
if (accountId != null) { sumSql.append(" AND account_id=?"); sumPs.add(accountId); }
if (kw != null && !kw.isBlank()) { sumSql.append(" AND (remark LIKE ? OR category LIKE ?)"); sumPs.add('%'+kw+'%'); sumPs.add('%'+kw+'%'); }
if (startDate != null && !startDate.isBlank()) { sumSql.append(" AND tx_time>=?"); sumPs.add(java.sql.Timestamp.valueOf(startDate + " 00:00:00")); }
if (endDate != null && !endDate.isBlank()) { sumSql.append(" AND tx_time<=?"); sumPs.add(java.sql.Timestamp.valueOf(endDate + " 23:59:59")); }
if (type != null && !type.isBlank()) { sumSql.append(" AND ot.`type`=?"); sumPs.add(type); }
if (accountId != null) { sumSql.append(" AND ot.account_id=?"); sumPs.add(accountId); }
if (kw != null && !kw.isBlank()) { sumSql.append(" AND (ot.remark LIKE ? OR ot.category LIKE ?)"); sumPs.add('%'+kw+'%'); sumPs.add('%'+kw+'%'); }
if (startDate != null && !startDate.isBlank()) { sumSql.append(" AND ot.tx_time>=?"); sumPs.add(java.sql.Timestamp.valueOf(startDate + " 00:00:00")); }
if (endDate != null && !endDate.isBlank()) { sumSql.append(" AND ot.tx_time<=?"); sumPs.add(java.sql.Timestamp.valueOf(endDate + " 23:59:59")); }
java.math.BigDecimal total = jdbcTemplate.queryForObject(sumSql.toString(), java.math.BigDecimal.class, sumPs.toArray());
java.util.Map<String,Object> resp = new java.util.HashMap<>(); resp.put("list", list); resp.put("totalAmount", total == null ? java.math.BigDecimal.ZERO : total); return resp;
}
@org.springframework.transaction.annotation.Transactional
public java.util.Map<String, Object> createOtherTransaction(Long shopId, Long userId, OrderDtos.CreateOtherTransactionRequest req) {
if (req == null) throw new IllegalArgumentException("请求为空");
String type = req.type == null ? null : req.type.trim().toLowerCase();
if (!"income".equals(type) && !"expense".equals(type)) throw new IllegalArgumentException("type 仅支持 income/expense");
if (req.accountId == null) throw new IllegalArgumentException("账户必选");
java.math.BigDecimal amt = n(req.amount);
if (amt.compareTo(java.math.BigDecimal.ZERO) <= 0) throw new IllegalArgumentException("金额需大于0");
java.time.LocalDateTime when;
if (req.txTime == null || req.txTime.isBlank()) when = nowUtc();
else {
// 允许 yyyy-MM-dd 或完整时间
try {
if (req.txTime.length() == 10) when = java.time.LocalDate.parse(req.txTime).atStartOfDay();
else when = java.time.LocalDateTime.parse(req.txTime);
} catch (Exception e) { when = nowUtc(); }
}
final java.sql.Timestamp whenTs = java.sql.Timestamp.from(when.atZone(java.time.ZoneOffset.UTC).toInstant());
// 插入 other_transactions
org.springframework.jdbc.support.GeneratedKeyHolder kh = new org.springframework.jdbc.support.GeneratedKeyHolder();
String sql = "INSERT INTO other_transactions (shop_id,user_id,`type`,category,counterparty_type,counterparty_id,account_id,amount,tx_time,remark,created_at,updated_at) " +
"VALUES (?,?,?,?,?,?,?,?,?, ?, NOW(), NOW())";
jdbcTemplate.update(con -> {
java.sql.PreparedStatement ps = con.prepareStatement(sql, new String[]{"id"});
int i = 1;
ps.setLong(i++, shopId);
ps.setLong(i++, userId);
ps.setString(i++, type);
ps.setString(i++, req.category);
if (req.counterpartyType == null) ps.setNull(i++, java.sql.Types.VARCHAR); else ps.setString(i++, req.counterpartyType);
if (req.counterpartyId == null) ps.setNull(i++, java.sql.Types.BIGINT); else ps.setLong(i++, req.counterpartyId);
ps.setLong(i++, req.accountId);
ps.setBigDecimal(i++, scale2(amt));
ps.setTimestamp(i++, whenTs);
ps.setString(i, req.remark);
return ps;
}, kh);
Number key = kh.getKey();
Long id = key == null ? null : key.longValue();
// 写支付流水,联动账户余额
String direction = "income".equals(type) ? "in" : "out";
org.springframework.jdbc.support.GeneratedKeyHolder payKh = new org.springframework.jdbc.support.GeneratedKeyHolder();
final Long idForPayment = id;
jdbcTemplate.update(con -> {
java.sql.PreparedStatement ps = con.prepareStatement(
"INSERT INTO payments (shop_id,user_id,biz_type,biz_id,account_id,direction,amount,pay_time,remark,created_at) VALUES (?,?,?,?,?,?,?,?,?,NOW())",
new String[]{"id"}
);
int i = 1;
ps.setLong(i++, shopId);
ps.setLong(i++, userId);
ps.setString(i++, "other");
if (idForPayment == null) ps.setNull(i++, java.sql.Types.BIGINT); else ps.setLong(i++, idForPayment);
ps.setLong(i++, req.accountId);
ps.setString(i++, direction);
ps.setBigDecimal(i++, scale2(amt));
ps.setTimestamp(i++, whenTs);
ps.setString(i, req.remark);
return ps;
}, payKh);
// 联动账户余额:收入加,支出减
java.math.BigDecimal delta = "income".equals(type) ? amt : amt.negate();
jdbcTemplate.update("UPDATE accounts SET balance = balance + ?, updated_at=NOW() WHERE id=? AND shop_id=?", scale2(delta), req.accountId, shopId);
java.util.Map<String,Object> resp = new java.util.HashMap<>();
resp.put("id", id);
return resp;
}
public java.util.Map<String,Object> listInventoryLogs(Long shopId, Long productId, String reason, String kw, int page, int size, String startDate, String endDate) {
StringBuilder sql = new StringBuilder("SELECT id, product_id, qty_delta, amount_delta, reason, tx_time FROM inventory_movements WHERE shop_id=?");
StringBuilder sql = new StringBuilder("SELECT id, product_id, qty_delta, amount_delta, COALESCE(amount_delta,0) AS amount, reason, tx_time AS txTime, remark FROM inventory_movements WHERE shop_id=?");
java.util.List<Object> ps = new java.util.ArrayList<>(); ps.add(shopId);
if (productId != null) { sql.append(" AND product_id=?"); ps.add(productId); }
if (reason != null && !reason.isBlank()) { sql.append(" AND reason=?"); ps.add(reason); }
@@ -354,6 +456,50 @@ public class OrderService {
java.util.Map<String,Object> resp = new java.util.HashMap<>(); resp.put("list", list); resp.put("totalAmount", total == null ? java.math.BigDecimal.ZERO : total); return resp;
}
// 详情:销售单
public java.util.Map<String,Object> getSalesOrderDetail(Long shopId, Long id) {
java.util.List<java.util.Map<String,Object>> heads = jdbcTemplate.queryForList(
"SELECT so.id, so.order_no AS orderNo, so.order_time AS orderTime, so.status, so.amount, so.paid_amount AS paidAmount, so.customer_id AS customerId, c.name AS customerName, so.remark\n" +
"FROM sales_orders so LEFT JOIN customers c ON c.id=so.customer_id WHERE so.shop_id=? AND so.id=?",
shopId, id);
if (heads.isEmpty()) return java.util.Map.of();
java.util.Map<String,Object> head = heads.get(0);
java.util.List<java.util.Map<String,Object>> items = jdbcTemplate.queryForList(
"SELECT i.id, i.product_id AS productId, p.name, p.spec, i.quantity, i.unit_price AS unitPrice, i.discount_rate AS discountRate, i.amount\n" +
"FROM sales_order_items i JOIN products p ON p.id=i.product_id WHERE i.order_id=?",
id);
java.util.List<java.util.Map<String,Object>> pays = jdbcTemplate.queryForList(
"SELECT p.id, p.amount, p.pay_time AS payTime, p.account_id AS accountId, a.name AS accountName, p.direction\n" +
"FROM payments p LEFT JOIN accounts a ON a.id=p.account_id WHERE p.biz_type='sale' AND p.biz_id=?",
id);
java.util.Map<String,Object> resp = new java.util.HashMap<>(head);
resp.put("items", items);
resp.put("payments", pays);
return resp;
}
// 详情:进货单
public java.util.Map<String,Object> getPurchaseOrderDetail(Long shopId, Long id) {
java.util.List<java.util.Map<String,Object>> heads = jdbcTemplate.queryForList(
"SELECT po.id, po.order_no AS orderNo, po.order_time AS orderTime, po.status, po.amount, po.paid_amount AS paidAmount, po.supplier_id AS supplierId, s.name AS supplierName, po.remark\n" +
"FROM purchase_orders po LEFT JOIN suppliers s ON s.id=po.supplier_id WHERE po.shop_id=? AND po.id=?",
shopId, id);
if (heads.isEmpty()) return java.util.Map.of();
java.util.Map<String,Object> head = heads.get(0);
java.util.List<java.util.Map<String,Object>> items = jdbcTemplate.queryForList(
"SELECT i.id, i.product_id AS productId, p.name, p.spec, i.quantity, i.unit_price AS unitPrice, i.amount\n" +
"FROM purchase_order_items i JOIN products p ON p.id=i.product_id WHERE i.order_id=?",
id);
java.util.List<java.util.Map<String,Object>> pays = jdbcTemplate.queryForList(
"SELECT p.id, p.amount, p.pay_time AS payTime, p.account_id AS accountId, a.name AS accountName, p.direction\n" +
"FROM payments p LEFT JOIN accounts a ON a.id=p.account_id WHERE p.biz_type='purchase' AND p.biz_id=?",
id);
java.util.Map<String,Object> resp = new java.util.HashMap<>(head);
resp.put("items", items);
resp.put("payments", pays);
return resp;
}
private static BigDecimal n(BigDecimal v) { return v == null ? BigDecimal.ZERO : v; }
private static BigDecimal scale2(BigDecimal v) { return v.setScale(2, java.math.RoundingMode.HALF_UP); }
private static LocalDateTime nowUtc() { return LocalDateTime.now(java.time.Clock.systemUTC()); }
@@ -364,10 +510,11 @@ public class OrderService {
}
private void ensureDefaultAccounts(Long shopId, Long userId) {
// 为 cash/bank/wechat 分别确保存在一条账户记录;按 type→name 顺序检查,避免同名唯一冲突
ensureAccount(shopId, userId, "cash", "现金");
ensureAccount(shopId, userId, "bank", "银行存款");
ensureAccount(shopId, userId, "wechat", "微信");
// 为 cash/bank/wechat/alipay 分别确保存在一条账户记录;按 type→name 顺序检查,避免同名唯一冲突
ensureAccount(shopId, userId, "cash", accountDefaults.getCashName());
ensureAccount(shopId, userId, "bank", accountDefaults.getBankName());
ensureAccount(shopId, userId, "wechat", accountDefaults.getWechatName());
ensureAccount(shopId, userId, "alipay", accountDefaults.getAlipayName());
}
private void ensureAccount(Long shopId, Long userId, String type, String name) {
@@ -383,8 +530,11 @@ public class OrderService {
String type = "cash";
if ("bank".equalsIgnoreCase(method)) type = "bank";
if ("wechat".equalsIgnoreCase(method)) type = "wechat";
String name = "现金";
if ("bank".equals(type)) name = "银行存款"; else if ("wechat".equals(type)) name = "微信";
if ("alipay".equalsIgnoreCase(method)) type = "alipay";
String name = accountDefaults.getCashName();
if ("bank".equals(type)) name = accountDefaults.getBankName();
else if ("wechat".equals(type)) name = accountDefaults.getWechatName();
else if ("alipay".equals(type)) name = accountDefaults.getAlipayName();
// 先按 type 查
List<Long> byType = jdbcTemplate.query("SELECT id FROM accounts WHERE shop_id=? AND type=? LIMIT 1", (rs,rn)->rs.getLong(1), shopId, type);
if (!byType.isEmpty()) return byType.get(0);

View File

@@ -39,6 +39,17 @@ public class OrderDtos {
public java.util.List<Long> paymentIds;
public CreatePaymentsResponse(java.util.List<Long> ids) { this.paymentIds = ids; }
}
public static class CreateOtherTransactionRequest {
public String type; // income | expense
public String category; // 分类key
public String counterpartyType; // customer | supplier | other
public Long counterpartyId; // 可空
public Long accountId; // 必填
public java.math.BigDecimal amount; // 必填,>0
public String txTime; // yyyy-MM-dd 或 ISO8601
public String remark; // 可空
}
}

View File

@@ -4,7 +4,7 @@ spring.application.name=demo
# 正确的配置
# 格式为: jdbc:mysql://<主机名>:<端口号>/<数据库名>?参数
# 默认附带 MySQL 8 推荐参数,避免握手/时区/编码问题
spring.datasource.url=${DB_URL:jdbc:mysql://mysql.tonaspace.com:3306/partsinquiry?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf8}
spring.datasource.url=${DB_URL:jdbc:mysql://mysql.tonaspace.com:3306/partsinquiry?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf8mb4&connectionCollation=utf8mb4_0900_ai_ci}
# 用户名和密码直接写值
spring.datasource.username=${DB_USER:root}
@@ -16,6 +16,10 @@ spring.jpa.open-in-view=false
spring.jpa.show-sql=false
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect
# 日志级别(开发调试)
logging.level.com.example.demo=DEBUG
logging.level.org.springframework.web.filter.CommonsRequestLoggingFilter=INFO
# CORS 简单放开(如需跨域)
spring.web.cors.allowed-origins=${CORS_ALLOWED_ORIGINS:*}
spring.web.cors.allowed-methods=GET,POST,PUT,DELETE,OPTIONS
@@ -38,3 +42,13 @@ attachments.placeholder.url-path=${ATTACHMENTS_PLACEHOLDER_URL:/api/attachments/
# 应用默认上下文(用于开发/演示环境)
app.defaults.shop-id=${APP_DEFAULT_SHOP_ID:1}
app.defaults.user-id=${APP_DEFAULT_USER_ID:2}
# 财务分类默认配置(前端请调用 /api/finance/categories 获取,禁止硬编码)
app.finance.income-categories=${APP_FINANCE_INCOME:operation_income:经营所得,interest_income:利息收入,other_income:其它收入,deposit_ar_income:收订金/欠款,investment_income:投资收入,sale_income:销售收入,account_operation:账户操作,fund_transfer_in:资金转账转入}
app.finance.expense-categories=${APP_FINANCE_EXPENSE:operation_expense:经营支出,office_supplies:办公用品,rent:房租,interest_expense:利息支出,other_expense:其它支出,account_operation:账户操作,fund_transfer_out:资金转账转出}
# 账户默认名称(避免硬编码,可被环境变量覆盖)
app.account.defaults.cash-name=${APP_ACCOUNT_CASH_NAME:现金}
app.account.defaults.bank-name=${APP_ACCOUNT_BANK_NAME:银行存款}
app.account.defaults.wechat-name=${APP_ACCOUNT_WECHAT_NAME:微信}
app.account.defaults.alipay-name=${APP_ACCOUNT_ALIPAY_NAME:支付宝}

View File

@@ -264,6 +264,8 @@
| user_id | BIGINT UNSIGNED | NOT NULL | | |
| name | VARCHAR(64) | NOT NULL | | |
| type | ENUM('cash','bank','alipay','wechat','other') | NOT NULL | cash | |
| bank_name | VARCHAR(64) | YES | | 银行名称type=bank 可用) |
| bank_account | VARCHAR(64) | YES | | 银行账号type=bank 可用) |
| balance | DECIMAL(18,2) | NOT NULL | 0.00 | |
| status | TINYINT UNSIGNED | NOT NULL | 1 | |
| created_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | |
@@ -379,6 +381,22 @@
**Indexes**: - PRIMARY KEY: `id` - KEY: `idx_ot_shop_time` (`shop_id`,`tx_time`) - KEY: `idx_ot_account` (`account_id`)
**Foreign Keys**: - `fk_ot_shop`: `shop_id``shops(id)` - `fk_ot_user`: `user_id``users(id)` - `fk_ot_account`: `account_id``accounts(id)`
### finance_categories
| Column Name | Data Type | Nullable | Default | Comment |
| ----------- | --------- | -------- | ------- | ------- |
| id | BIGINT UNSIGNED | NOT NULL | AUTO_INCREMENT | |
| shop_id | BIGINT UNSIGNED | NOT NULL | | |
| type | ENUM('income','expense') | NOT NULL | | 分类类型 |
| key | VARCHAR(64) | NOT NULL | | 分类键(稳定标识) |
| label | VARCHAR(64) | NOT NULL | | 分类名称(支持中文) |
| sort_order | INT | NOT NULL | 0 | 排序 |
| status | TINYINT UNSIGNED | NOT NULL | 1 | 1启用 0停用 |
| created_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | |
| updated_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | |
**Indexes**: - PRIMARY KEY: `id` - UNIQUE: `ux_finance_cat` (`shop_id`,`type`,`key`) - KEY: `idx_finance_cat_shop_type` (`shop_id`,`type`)
**Foreign Keys**: - `fk_finance_cat_shop`: `shop_id``shops(id)`
### 触发器
- `trg_products_bi`: BEFORE INSERT ON `products` → 设置 `products.search_text`
- `trg_products_au`: BEFORE UPDATE ON `products` → 维护 `products.search_text`

View File

@@ -7,6 +7,38 @@ info:
servers:
- url: /
paths:
/api/finance/categories:
get:
summary: 财务分类(✅ Fully Implemented
description: 返回其他收入/支出的分类。读取顺序finance_categories 表 → system_parameters → app.finance.* 默认配置前端已接入开单页分类chips。
parameters:
- in: header
name: X-Shop-Id
required: false
schema: { type: integer }
description: 店铺ID缺省 1
responses:
'200':
description: 成功
content:
application/json:
schema:
type: object
properties:
incomeCategories:
type: array
items:
type: object
properties:
key: { type: string }
label: { type: string }
expenseCategories:
type: array
items:
type: object
properties:
key: { type: string }
label: { type: string }
/api/dashboard/overview:
get:
summary: 首页概览(✅ Fully Implemented
@@ -65,7 +97,21 @@ paths:
/api/accounts:
get:
summary: 账户列表(❌ Partially Implemented
description: 前端账户选择页已接入,后端返回数组或 {list:[]} 皆可
description: 返回启用账户列表;支持数组或 {list:[]} 两种返回格式以兼容前端
parameters:
- in: query
name: kw
schema: { type: string }
- in: query
name: status
schema: { type: integer, enum: [0,1] }
description: 仅后台使用;前端默认不传即等同 status=1
- in: query
name: page
schema: { type: integer, default: 1 }
- in: query
name: size
schema: { type: integer, default: 50 }
responses:
'200':
description: 成功
@@ -82,6 +128,79 @@ paths:
type: array
items:
$ref: '#/components/schemas/Account'
post:
summary: 新增账户(❌ Partially Implemented
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateAccountRequest'
responses:
'200': { description: 成功 }
/api/accounts/{id}:
put:
summary: 更新账户(❌ Partially Implemented
parameters:
- in: path
name: id
required: true
schema: { type: integer, format: int64 }
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateAccountRequest'
responses:
'200': { description: 成功 }
/api/accounts/{id}/ledger:
get:
summary: 账户流水(❌ Partially Implemented
parameters:
- in: path
name: id
required: true
schema: { type: integer, format: int64 }
- in: query
name: startDate
schema: { type: string, format: date }
- in: query
name: endDate
schema: { type: string, format: date }
- in: query
name: kw
schema: { type: string }
- in: query
name: page
schema: { type: integer, default: 1 }
- in: query
name: size
schema: { type: integer, default: 20 }
responses:
'200':
description: 成功
content:
application/json:
schema:
type: object
properties:
opening: { type: number }
income: { type: number }
expense: { type: number }
ending: { type: number }
list:
type: array
items:
type: object
properties:
id: { type: integer, format: int64 }
txTime: { type: string, format: date-time }
direction: { type: string, enum: [in, out] }
amount: { type: number }
src: { type: string, description: 'payments/other' }
category: { type: string, nullable: true }
remark: { type: string, nullable: true }
/api/suppliers:
get:
summary: 供应商搜索(✅ Fully Implemented
@@ -148,7 +267,7 @@ paths:
'200': { description: 成功 }
/api/other-transactions:
post:
summary: 新建其他收入/支出(❌ Partially Implemented
summary: 新建其他收入/支出(✅ Fully Implemented
requestBody:
required: true
content:
@@ -448,8 +567,8 @@ paths:
type: string
/api/orders:
get:
summary: 单据列表查询(❌ Partially Implemented
description: 支持按时间范围与关键字筛选;参数 biz=sale|purchasetype=out|in|return返回 {list:[]}。
summary: 单据列表查询(✅ Fully Implemented
description: 支持按时间范围与关键字筛选;参数 biz=sale|purchasetype=out|in|return返回 {list:[]}。后端已返回驼峰字段与名称sale: customerNamepurchase: supplierName
parameters:
- in: query
name: biz
@@ -500,7 +619,7 @@ paths:
paymentIds:
type: array
items: { type: integer, format: int64 }
description: 支持按时间范围与关键字筛选;返回 {list:[]}。前端已接入,后端待实现
description: 收/付款创建。注意根据约束sale/purchase 收/付款必须绑定订单orderId 不可为空)
parameters:
- in: query
name: kw
@@ -538,7 +657,7 @@ paths:
/api/purchase-orders:
get:
summary: 进货单列表查询(❌ Partially Implemented
summary: 进货单列表查询(✅ Fully Implemented
parameters:
- in: query
name: kw
@@ -581,7 +700,7 @@ paths:
/api/payments:
get:
summary: 收付款流水列表(❌ Partially Implemented
summary: 收付款流水列表(✅ Fully Implemented
parameters:
- in: query
name: kw
@@ -606,6 +725,7 @@ paths:
schema:
type: object
properties:
totalAmount: { type: number }
list:
type: array
items:
@@ -614,13 +734,13 @@ paths:
id: { type: integer, format: int64 }
bizType: { type: string }
direction: { type: string }
payTime: { type: string, format: date-time }
orderTime: { type: string, format: date-time, description: '支付时间,原 payTime' }
amount: { type: number }
accountName: { type: string }
/api/inventories/logs:
get:
summary: 库存/盘点流水列表(❌ Partially Implemented
summary: 库存/盘点流水列表(✅ Fully Implemented
parameters:
- in: query
name: kw
@@ -651,19 +771,116 @@ paths:
schema:
type: object
properties:
totalAmount: { type: number }
list:
type: array
items:
type: object
properties:
id: { type: integer, format: int64 }
bizType: { type: string }
txTime: { type: string, format: date-time }
amount: { type: number }
amount: { type: number, description: '金额变化若无则0' }
remark: { type: string }
productId: { type: integer, format: int64 }
qtyDelta: { type: number }
amountDelta: { type: number, nullable: true }
/api/orders/{id}:
get:
summary: 销售单详情(✅ Fully Implemented
parameters:
- in: path
name: id
required: true
schema: { type: integer, format: int64 }
responses:
'200':
description: 成功
content:
application/json:
schema:
type: object
properties:
id: { type: integer, format: int64 }
orderNo: { type: string }
orderTime: { type: string, format: date-time }
status: { type: string }
amount: { type: number }
paidAmount: { type: number }
customerId: { type: integer, format: int64, nullable: true }
customerName: { type: string, nullable: true }
remark: { type: string, nullable: true }
items:
type: array
items:
type: object
properties:
productId: { type: integer, format: int64 }
name: { type: string }
spec: { type: string }
quantity: { type: number }
unitPrice: { type: number }
discountRate: { type: number }
amount: { type: number }
payments:
type: array
items:
type: object
properties:
id: { type: integer, format: int64 }
amount: { type: number }
payTime: { type: string, format: date-time }
accountId: { type: integer, format: int64 }
accountName: { type: string }
direction: { type: string }
/api/purchase-orders/{id}:
get:
summary: 进货单详情(✅ Fully Implemented
parameters:
- in: path
name: id
required: true
schema: { type: integer, format: int64 }
responses:
'200':
description: 成功
content:
application/json:
schema:
type: object
properties:
id: { type: integer, format: int64 }
orderNo: { type: string }
orderTime: { type: string, format: date-time }
status: { type: string }
amount: { type: number }
paidAmount: { type: number }
supplierId: { type: integer, format: int64, nullable: true }
supplierName: { type: string, nullable: true }
remark: { type: string, nullable: true }
items:
type: array
items:
type: object
properties:
productId: { type: integer, format: int64 }
name: { type: string }
spec: { type: string }
quantity: { type: number }
unitPrice: { type: number }
amount: { type: number }
payments:
type: array
items:
type: object
properties:
id: { type: integer, format: int64 }
amount: { type: number }
payTime: { type: string, format: date-time }
accountId: { type: integer, format: int64 }
accountName: { type: string }
direction: { type: string }
/api/attachments:
post:
summary: 上传附件(✅ Fully Implemented占位图方案
@@ -772,6 +989,21 @@ components:
enum: [cash, bank, alipay, wechat, other]
balance:
type: number
bankName:
type: string
nullable: true
bankAccount:
type: string
nullable: true
CreateAccountRequest:
type: object
properties:
name: { type: string }
type: { type: string, enum: [cash, bank, alipay, wechat, other] }
bankName: { type: string, nullable: true }
bankAccount: { type: string, nullable: true }
openingBalance: { type: number, nullable: true }
status: { type: integer, enum: [0,1], default: 1 }
Supplier:
type: object
properties:
@@ -803,6 +1035,9 @@ components:
enum: [income, expense]
category:
type: string
counterpartyType:
type: string
enum: [customer, supplier, other]
counterpartyId:
type: integer
format: int64

View File

@@ -84,11 +84,47 @@
"navigationBarTitleText": "选择账户"
}
},
{
"path": "pages/account/ledger",
"style": {
"navigationBarTitleText": "账户流水"
}
},
{
"path": "pages/account/form",
"style": {
"navigationBarTitleText": "新增/编辑账户"
}
},
{
"path": "pages/detail/index",
"style": {
"navigationBarTitleText": "明细"
}
},
{
"path": "pages/my/index",
"style": {
"navigationBarTitleText": "我的"
}
},
{
"path": "pages/my/about",
"style": {
"navigationBarTitleText": "关于与协议"
}
},
{
"path": "pages/report/index",
"style": {
"navigationBarTitleText": "报表"
}
},
{
"path": "pages/report/entry",
"style": {
"navigationBarTitleText": "报表"
}
}
],
"globalStyle": {

View File

@@ -0,0 +1,74 @@
<template>
<view class="page">
<view class="form">
<view class="field"><text class="label">账户名称</text><input class="input" v-model="form.name" placeholder="必填"/></view>
<view class="field" @click="showType=true">
<text class="label">账户类型</text>
<text class="value">{{ typeLabel(form.type) }}</text>
</view>
<view v-if="form.type==='bank'" class="field"><text class="label">银行名称</text><input class="input" v-model="form.bankName" placeholder="选填"/></view>
<view v-if="form.type==='bank'" class="field"><text class="label">银行账号</text><input class="input" v-model="form.bankAccount" placeholder="选填"/></view>
<view class="field"><text class="label">当前余额</text><input class="input" type="number" v-model="form.openingBalance" placeholder="0.00"/></view>
</view>
<view class="actions">
<button class="primary" @click="save">保存</button>
</view>
<uni-popup ref="popup" type="bottom" v-model="showType">
<view class="sheet">
<view class="sheet-item" v-for="t in types" :key="t.key" @click="form.type=t.key;showType=false">{{ t.name }}</view>
<view class="sheet-cancel" @click="showType=false">取消</view>
</view>
</uni-popup>
</view>
</template>
<script>
import { post, put, get } from '../../common/http.js'
export default {
data(){
return {
id: null,
form: { name: '', type: 'cash', bankName: '', bankAccount: '', openingBalance: '' },
showType: false,
types: [
{ key: 'cash', name: '现金' },
{ key: 'bank', name: '银行存款' },
{ key: 'wechat', name: '微信' },
{ key: 'alipay', name: '支付宝' },
{ key: 'other', name: '其他' }
]
}
},
onLoad(q){ this.id = q && q.id ? Number(q.id) : null; if (this.id) this.load(); },
methods: {
typeLabel(t){ const m = {cash:'现金', bank:'银行存款', wechat:'微信', alipay:'支付宝', other:'其他'}; return m[t]||t },
async load(){ try { const list = await get('/api/accounts'); const a = (Array.isArray(list)?list:(list?.list||[])).find(x=>x.id==this.id); if (a) { this.form={ name:a.name, type:a.type, bankName:a.bank_name||a.bankName||'', bankAccount:a.bank_account||a.bankAccount||'', openingBalance:'' } } } catch(e){} },
async save(){
if (!this.form.name) { uni.showToast({ title: '请输入名称', icon: 'none' }); return }
try {
const body = { ...this.form, openingBalance: Number(this.form.openingBalance||0) }
if (this.id) await put(`/api/accounts/${this.id}`, body)
else await post('/api/accounts', { ...body, status: 1 })
uni.showToast({ title: '已保存', icon: 'success' })
setTimeout(()=>uni.navigateBack(), 300)
} catch(e) { uni.showToast({ title: '保存失败', icon: 'none' }) }
}
}
}
</script>
<style>
.page { display:flex; flex-direction: column; height: 100vh; }
.form { background:#fff; }
.field { display:flex; align-items:center; justify-content: space-between; padding: 18rpx 20rpx; border-bottom:1rpx solid #f3f3f3; }
.label { color:#666; }
.input { flex:1; text-align: right; color:#333; }
.value { color:#333; }
.actions { margin-top: 20rpx; padding: 0 20rpx; }
.primary { width: 100%; background: #3c9cff; color:#fff; border-radius: 8rpx; padding: 22rpx 0; }
.sheet { background:#fff; }
.sheet-item { padding: 26rpx; text-align:center; border-bottom:1rpx solid #f2f2f2; }
.sheet-cancel { padding: 26rpx; text-align:center; color:#666; }
</style>

View File

@@ -0,0 +1,87 @@
<template>
<view class="page">
<view class="filters">
<picker mode="date" :value="startDate" @change="e=>{startDate=e.detail.value;load()}">
<view class="field"><text class="label">开始</text><text class="value">{{ startDate || '—' }}</text></view>
</picker>
<picker mode="date" :value="endDate" @change="e=>{endDate=e.detail.value;load()}">
<view class="field"><text class="label">结束</text><text class="value">{{ endDate || '—' }}</text></view>
</picker>
</view>
<view class="summary">
<view class="sum-item"><text class="k">收入</text><text class="v">{{ fmt(income) }}</text></view>
<view class="sum-item"><text class="k">支出</text><text class="v">{{ fmt(expense) }}</text></view>
<view class="sum-item"><text class="k">期初</text><text class="v">{{ fmt(opening) }}</text></view>
<view class="sum-item"><text class="k">期末</text><text class="v">{{ fmt(ending) }}</text></view>
</view>
<scroll-view scroll-y class="list">
<view class="item" v-for="it in list" :key="it.id">
<view class="row">
<text class="title">{{ it.src==='other' ? (it.category || '其他') : (it.remark || '收付款') }}</text>
<text class="amount" :class="{ in: it.direction==='in', out: it.direction==='out' }">{{ it.direction==='in' ? '+' : '-' }}{{ fmt(it.amount) }}</text>
</view>
<view class="meta">{{ formatDate(it.tx_time || it.txTime) }} · {{ it.remark || '-' }}</view>
</view>
</scroll-view>
</view>
</template>
<script>
import { get } from '../../common/http.js'
export default {
data() {
return { accountId: null, startDate: '', endDate: '', list: [], opening: 0, income: 0, expense: 0, ending: 0 }
},
onLoad(query) {
this.accountId = Number(query && query.id)
this.quickInit()
this.load()
},
methods: {
quickInit() {
// 默认本月
const now = new Date()
const y = now.getFullYear(), m = now.getMonth()+1
this.startDate = `${y}-${String(m).padStart(2,'0')}-01`
const lastDay = new Date(y, m, 0).getDate()
this.endDate = `${y}-${String(m).padStart(2,'0')}-${String(lastDay).padStart(2,'0')}`
},
async load(page=1, size=50) {
try {
const res = await get(`/api/accounts/${this.accountId}/ledger`, { startDate: this.startDate, endDate: this.endDate, page, size })
this.list = (res && res.list) || []
this.opening = Number(res && res.opening || 0)
this.income = Number(res && res.income || 0)
this.expense = Number(res && res.expense || 0)
this.ending = Number(res && res.ending || 0)
} catch (e) {
uni.showToast({ title: '加载失败', icon: 'none' })
}
},
fmt(v) { return (typeof v === 'number' ? v : Number(v||0)).toFixed(2) },
formatDate(s) { if (!s) return '-'; try { const d=new Date(s); const pad=n=>String(n).padStart(2,'0'); return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}` } catch(e){ return s } }
}
}
</script>
<style>
.page { display:flex; flex-direction: column; height: 100vh; }
.filters { display:flex; gap: 16rpx; padding: 16rpx; background:#fff; }
.field { display:flex; justify-content: space-between; align-items:center; padding: 16rpx; border:1rpx solid #eee; border-radius: 12rpx; min-width: 300rpx; }
.label { color:#666; }
.value { color:#333; }
.summary { display:grid; grid-template-columns: repeat(4,1fr); gap: 12rpx; padding: 12rpx 16rpx; background:#fff; border-top:1rpx solid #f1f1f1; border-bottom:1rpx solid #f1f1f1; }
.sum-item { padding: 12rpx; text-align:center; }
.k { display:block; color:#888; font-size: 24rpx; }
.v { display:block; margin-top:6rpx; font-weight:700; color:#333; }
.list { flex:1; }
.item { padding: 18rpx 16rpx; border-bottom:1rpx solid #f4f4f4; background:#fff; }
.row { display:flex; align-items:center; justify-content: space-between; margin-bottom: 6rpx; }
.title { color:#333; }
.amount { font-weight:700; }
.amount.in { color:#2a9d8f; }
.amount.out { color:#d35b5b; }
.meta { color:#999; font-size: 24rpx; }
</style>

View File

@@ -6,6 +6,7 @@
<view class="meta">{{ typeLabel(a.type) }} · 余额{{ a.balance?.toFixed ? a.balance.toFixed(2) : a.balance }}</view>
</view>
</scroll-view>
<view class="fab" @click="create"></view>
</view>
</template>
@@ -13,8 +14,9 @@
import { get } from '../../common/http.js'
const TYPE_MAP = { cash: '现金', bank: '银行', alipay: '支付宝', wechat: '微信', other: '其他' }
export default {
data() { return { accounts: [] } },
async onLoad() {
data() { return { accounts: [], mode: 'view' } },
async onLoad(q) {
this.mode = (q && q.mode) || 'view'
try {
const res = await get('/api/accounts')
this.accounts = Array.isArray(res) ? res : (res?.list || [])
@@ -22,13 +24,18 @@
},
methods: {
select(a) {
if (this.mode === 'pick') {
const opener = getCurrentPages()[getCurrentPages().length-2]
if (opener && opener.$vm) {
opener.$vm.selectedAccountId = a.id
opener.$vm.selectedAccountName = a.name
}
uni.navigateBack()
} else {
uni.navigateTo({ url: `/pages/account/ledger?id=${a.id}` })
}
},
create() { uni.navigateTo({ url: '/pages/account/form' }) },
typeLabel(t) { return TYPE_MAP[t] || t }
}
}
@@ -40,6 +47,7 @@
.item { padding: 20rpx 24rpx; background:#fff; border-bottom: 1rpx solid #f1f1f1; }
.name { color:#333; margin-bottom: 6rpx; }
.meta { color:#888; font-size: 24rpx; }
.fab { position: fixed; right: 32rpx; bottom: 120rpx; width: 100rpx; height: 100rpx; border-radius: 50%; background:#3c9cff; color:#fff; display:flex; align-items:center; justify-content:center; font-size: 52rpx; box-shadow: 0 10rpx 20rpx rgba(0,0,0,0.18); }
</style>

View File

@@ -78,10 +78,10 @@
<view class="tab" :class="{ active: activeTab==='detail' }" @click="goDetail">
<text>明细</text>
</view>
<view class="tab" :class="{ active: activeTab==='report' }" @click="activeTab='report'">
<view class="tab" :class="{ active: activeTab==='report' }" @click="goReport">
<text>报表</text>
</view>
<view class="tab" :class="{ active: activeTab==='me' }" @click="activeTab='me'">
<view class="tab" :class="{ active: activeTab==='me' }" @click="goMe">
<text>我的</text>
</view>
</view>
@@ -156,6 +156,11 @@
uni.navigateTo({ url: '/pages/customer/select' })
return
}
if (item.key === 'account') {
// 进入账户模块(先使用账户选择页,已对接后端 /api/accounts
uni.navigateTo({ url: '/pages/account/select' })
return
}
if (item.key === 'supplier') {
uni.navigateTo({ url: '/pages/supplier/select' })
return
@@ -174,6 +179,14 @@
try { console.log('[index] goDetail → /pages/detail/index') } catch(e){}
uni.navigateTo({ url: '/pages/detail/index' })
},
goReport() {
this.activeTab = 'report'
uni.navigateTo({ url: '/pages/report/entry' })
},
goMe() {
this.activeTab = 'me'
uni.navigateTo({ url: '/pages/my/index' })
},
onNoticeTap(n) {
uni.showModal({
title: '广告',

View File

@@ -0,0 +1,59 @@
<template>
<view class="about">
<view class="hero">
<image class="logo" src="/static/logo.png" mode="aspectFit" />
<text class="title">五金配件管家</text>
<text class="subtitle">专注小微门店的极简进销存</text>
</view>
<view class="card">
<view class="row">
<text class="label">版本</text>
<text class="value">1.0.0</text>
</view>
<view class="row">
<text class="label">隐私协议</text>
<text class="link" @click="openPolicy">查看</text>
</view>
<view class="row">
<text class="label">用户协议</text>
<text class="link" @click="openTerms">查看</text>
</view>
<view class="row">
<text class="label">个人信息安全投诉</text>
<text class="link" @click="openComplaint">提交</text>
</view>
</view>
</view>
</template>
<script>
export default {
methods: {
openPolicy() {
uni.showModal({ title: '隐私协议', content: '隐私协议(静态占位)', showCancel: false })
},
openTerms() {
uni.showModal({ title: '用户协议', content: '用户协议(静态占位)', showCancel: false })
},
openComplaint() {
uni.showToast({ title: '暂未开通', icon: 'none' })
}
}
}
</script>
<style>
.about { padding: 24rpx; }
.hero { padding: 32rpx 24rpx; display: flex; flex-direction: column; align-items: center; gap: 10rpx; }
.logo { width: 160rpx; height: 160rpx; border-radius: 32rpx; }
.title { margin-top: 8rpx; font-size: 36rpx; font-weight: 800; color: #333; }
.subtitle { font-size: 26rpx; color: #888; }
.card { margin-top: 18rpx; background: #fff; border-radius: 16rpx; overflow: hidden; }
.row { display: flex; align-items: center; padding: 24rpx; border-top: 1rpx solid #f2f2f2; }
.label { color: #666; }
.value { margin-left: auto; color: #333; }
.link { margin-left: auto; color: #1aad19; }
</style>

152
frontend/pages/my/index.vue Normal file
View File

@@ -0,0 +1,152 @@
<template>
<view class="me">
<view class="card user">
<image class="avatar" :src="avatarUrl" mode="aspectFill" @error="onAvatarError" />
<view class="meta">
<text class="name">{{ shopName }}</text>
<text class="phone">{{ mobileDisplay }}</text>
<text class="role">老板</text>
</view>
</view>
<view class="group">
<view class="group-title">会员与订单</view>
<view class="cell" @click="goVip">
<text>VIP会员</text>
<text class="arrow"></text>
</view>
<view class="cell" @click="goMyOrders">
<text>我的订单</text>
<text class="arrow"></text>
</view>
</view>
<view class="group">
<view class="group-title">基础管理</view>
<view class="cell" @click="goSupplier">
<text>供应商管理</text>
<text class="arrow"></text>
</view>
<view class="cell" @click="goCustomer">
<text>客户管理</text>
<text class="arrow"></text>
</view>
<view class="cell" @click="goCustomerQuote">
<text>客户报价</text>
<text class="arrow"></text>
</view>
<view class="cell" @click="goShop">
<text>店铺管理</text>
<text class="arrow"></text>
</view>
</view>
<view class="group">
<view class="group-title">设置中心</view>
<view class="cell" @click="editProfile">
<text>账号与安全</text>
<text class="desc">修改头像姓名密码</text>
<text class="arrow"></text>
</view>
<view class="cell" @click="goProductSettings">
<text>商品设置</text>
<text class="arrow"></text>
</view>
<view class="cell" @click="goSystemParams">
<text>系统参数</text>
<text class="desc">低价提示默认收款单行折扣等</text>
<text class="arrow"></text>
</view>
<view class="cell" @click="goAbout">
<text>关于与协议</text>
<text class="arrow"></text>
</view>
<view class="cell danger" @click="logout">
<text>退出登录</text>
</view>
</view>
</view>
</template>
<script>
import { get } from '../../common/http.js'
export default {
data() {
return {
avatarUrl: '/static/logo.png',
shopName: '我的店铺',
mobile: ''
}
},
onLoad() {
this.fetchProfile()
},
computed: {
mobileDisplay() {
const m = String(this.mobile || '')
return m.length === 11 ? m.slice(0,3) + '****' + m.slice(7) : (m || '未绑定手机号')
}
},
methods: {
async fetchProfile() {
// 后端暂无专门店铺/用户信息接口,先使用概览接口作为在线性检测与占位数据来源
try { await get('/api/dashboard/overview') } catch(e) {}
// 读取本地可能保存的店铺名与头像
try {
const storeName = uni.getStorageSync('SHOP_NAME') || ''
const avatar = uni.getStorageSync('USER_AVATAR') || ''
const phone = uni.getStorageSync('USER_MOBILE') || ''
if (storeName) this.shopName = storeName
if (avatar) this.avatarUrl = avatar
this.mobile = phone
} catch(e) {}
},
onAvatarError() {
this.avatarUrl = '/static/logo.png'
},
goVip() { uni.showToast({ title: 'VIP会员开发中', icon: 'none' }) },
goMyOrders() { uni.navigateTo({ url: '/pages/detail/index' }) },
goSupplier() { uni.navigateTo({ url: '/pages/supplier/select' }) },
goCustomer() { uni.navigateTo({ url: '/pages/customer/select' }) },
goCustomerQuote() { uni.showToast({ title: '客户报价(开发中)', icon: 'none' }) },
goShop() { uni.showToast({ title: '店铺管理(开发中)', icon: 'none' }) },
editProfile() { uni.showToast({ title: '账号与安全(开发中)', icon: 'none' }) },
goProductSettings() { uni.navigateTo({ url: '/pages/product/settings' }) },
goSystemParams() { uni.showToast({ title: '系统参数(开发中)', icon: 'none' }) },
goAbout() { uni.navigateTo({ url: '/pages/my/about' }) },
logout() {
try {
uni.removeStorageSync('TOKEN')
uni.removeStorageSync('USER_AVATAR')
uni.removeStorageSync('USER_NAME')
uni.removeStorageSync('USER_MOBILE')
uni.removeStorageSync('SHOP_NAME')
uni.showToast({ title: '已退出', icon: 'none' })
setTimeout(() => { uni.reLaunch({ url: '/pages/index/index' }) }, 300)
} catch(e) {
uni.reLaunch({ url: '/pages/index/index' })
}
}
}
}
</script>
<style>
.me { padding: 24rpx; }
.card.user { display: flex; gap: 18rpx; padding: 22rpx; background: #fff; border-radius: 16rpx; box-shadow: 0 6rpx 16rpx rgba(0,0,0,0.06); align-items: center; }
.avatar { width: 120rpx; height: 120rpx; border-radius: 60rpx; background: #f5f5f5; }
.meta { display: flex; flex-direction: column; gap: 6rpx; }
.name { font-size: 34rpx; font-weight: 700; color: #333; }
.phone { font-size: 26rpx; color: #888; }
.role { font-size: 22rpx; color: #999; }
.group { margin-top: 24rpx; background: #fff; border-radius: 16rpx; overflow: hidden; }
.group-title { padding: 18rpx 22rpx; font-size: 26rpx; color: #999; background: #fafafa; }
.cell { display: flex; align-items: center; padding: 26rpx 22rpx; border-top: 1rpx solid #f0f0f0; color: #333; }
.cell .desc { margin-left: auto; margin-right: 8rpx; font-size: 22rpx; color: #999; }
.cell .arrow { margin-left: auto; color: #bbb; }
.cell.danger { color: #dd524d; justify-content: center; font-weight: 700; }
</style>

View File

@@ -89,6 +89,11 @@
<!-- 其它收入/支出 表单 -->
<view v-else>
<!-- 往来单位类型切换 -->
<view class="subtabs">
<button class="subbtn" :class="{ active: counterpartyType==='customer' }" @click="setCounterparty('customer')">客户</button>
<button class="subbtn" :class="{ active: counterpartyType==='supplier' }" @click="setCounterparty('supplier')">供应商</button>
</view>
<view class="chips">
<view v-for="c in (biz==='income' ? incomeCategories : expenseCategories)" :key="c.key" class="chip" :class="{ active: activeCategory===c.key }" @click="activeCategory=c.key">{{ c.label }}</view>
</view>
@@ -168,6 +173,7 @@
supplierName: '',
items: [],
activeCategory: 'sale_income',
counterpartyType: 'customer',
trxAmount: 0,
selectedAccountId: null,
selectedAccountName: '',
@@ -185,16 +191,19 @@
},
customerLabel() { return this.customerName || '零售客户' },
supplierLabel() { return this.supplierName || '零散供应商' },
incomeCategories() { return INCOME_CATEGORIES },
expenseCategories() { return EXPENSE_CATEGORIES },
incomeCategories() { return this._incomeCategories || INCOME_CATEGORIES },
expenseCategories() { return this._expenseCategories || EXPENSE_CATEGORIES },
accountLabel() { return this.selectedAccountName || '现金' },
counterpartyLabel() { return this.customerName || this.supplierName || '—' },
counterpartyLabel() { return this.counterpartyType==='customer' ? (this.customerName || '—') : (this.supplierName || '—') },
// 收款/付款合计
payTotal() {
const p = this.payments || { cash:0, bank:0, wechat:0 }
return Number(p.cash||0) + Number(p.bank||0) + Number(p.wechat||0)
}
},
onLoad() {
this.fetchCategories()
},
onShow() {
if (this.biz === 'sale') {
if (this.order.customerId && this.order.customerId !== this._lastCustomerId) {
@@ -207,6 +216,20 @@
}
},
methods: {
async fetchCategories() {
try {
const res = await get('/api/finance/categories')
if (res && Array.isArray(res.incomeCategories)) this._incomeCategories = res.incomeCategories
if (res && Array.isArray(res.expenseCategories)) this._expenseCategories = res.expenseCategories
this.ensureActiveCategory()
} catch (_) { this.ensureActiveCategory() }
},
ensureActiveCategory() {
const list = this.biz==='income' ? (this.incomeCategories||[]) : (this.expenseCategories||[])
if (!list.length) return
const exists = list.some(it => it && it.key === this.activeCategory)
if (!exists) this.activeCategory = list[0].key
},
async loadCustomerLevel(customerId) {
try {
const d = await get(`/api/customers/${customerId}`)
@@ -235,7 +258,7 @@
this.recalc()
},
onPriceInput(it) { if (it) { it._autoPrice = false; this.recalc() } },
switchBiz(type) { this.biz = type },
switchBiz(type) { this.biz = type; this.ensureActiveCategory() },
onDateChange(e) { this.order.orderTime = e.detail.value },
chooseCustomer() {
uni.navigateTo({ url: '/pages/customer/select' })
@@ -244,12 +267,13 @@
chooseProduct() {
uni.navigateTo({ url: '/pages/product/select' })
},
chooseAccount() { uni.navigateTo({ url: '/pages/account/select' }) },
chooseAccount() { uni.navigateTo({ url: '/pages/account/select?mode=pick' }) },
chooseCounterparty() {
if (this.biz==='income' || this.biz==='expense') {
uni.navigateTo({ url: '/pages/customer/select' })
}
if (!(this.biz==='income' || this.biz==='expense')) return
if (this.counterpartyType==='customer') { uni.navigateTo({ url: '/pages/customer/select' }) }
else { uni.navigateTo({ url: '/pages/supplier/select' }) }
},
setCounterparty(t) { this.counterpartyType = t; this.ensureActiveCategory() },
recalc() { this.$forceUpdate() },
recalcPay() { this.$forceUpdate() },
async submit() {
@@ -276,7 +300,8 @@
}) : {
type: this.biz,
category: this.activeCategory,
counterpartyId: this.order.customerId || null,
counterpartyType: this.counterpartyType,
counterpartyId: this.counterpartyType==='customer' ? (this.order.customerId || null) : (this.order.supplierId || null),
accountId: this.selectedAccountId || null,
amount: Number(this.trxAmount||0),
txTime: this.order.orderTime,
@@ -328,6 +353,10 @@
.textarea { position: relative; padding: 16rpx 24rpx; background:#fff; border-top: 1rpx solid #eee; }
.amount-badge { position: absolute; right: 24rpx; top: -36rpx; background: #d1f0ff; color:#107e9b; padding: 8rpx 16rpx; border-radius: 12rpx; font-size: 24rpx; }
.date-mini { position: absolute; right: 24rpx; bottom: 20rpx; color:#666; font-size: 24rpx; }
/* 分类chips样式选中后文字变红 */
.chips { display:flex; flex-wrap: wrap; gap: 12rpx; padding: 12rpx 24rpx; }
.chip { padding: 10rpx 20rpx; border-radius: 999rpx; background: #f4f4f4; color:#666; }
.chip.active { color: #e54d42; }
</style>

View File

@@ -0,0 +1,44 @@
<template>
<view class="entry">
<view class="section">
<view class="section-title">资金报表</view>
<view class="grid">
<view class="btn" @click="go('sale','customer')">利润统计</view>
<view class="btn" @click="go('sale','product')">营业员统计</view>
<view class="btn" @click="go('sale','customer')">经营业绩</view>
</view>
</view>
<view class="section">
<view class="section-title">进销存报表</view>
<view class="grid">
<view class="btn" @click="go('sale','customer')">销售统计</view>
<view class="btn" @click="go('purchase','supplier')">进货统计</view>
<view class="btn" @click="go('inventory','qty')">库存统计</view>
<view class="btn" @click="go('arap','ar')">应收对账单</view>
<view class="btn" @click="go('arap','ap')">应付对账单</view>
</view>
</view>
</view>
</template>
<script>
export default {
methods: {
go(mode, dim) {
const q = `mode=${encodeURIComponent(mode)}&dim=${encodeURIComponent(dim||'')}`
uni.navigateTo({ url: `/pages/report/index?${q}` })
}
}
}
</script>
<style>
.entry { padding: 20rpx; }
.section { margin-bottom: 24rpx; }
.section-title { background:#f1f4f8; color:#6a7a8a; padding: 14rpx 16rpx; border-radius: 12rpx; font-weight: 700; }
.grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 18rpx; padding: 18rpx 6rpx 0; }
.btn { text-align: center; padding: 18rpx 8rpx; border: 1rpx solid #e5e9ef; border-radius: 12rpx; color:#333; background: #fff; }
.btn:active { background: #f6f8fa; }
</style>

View File

@@ -0,0 +1,291 @@
<template>
<view class="report">
<view class="modes">
<view class="mode-tab" :class="{active: mode==='sale'}" @click="setMode('sale')">销售统计</view>
<view class="mode-tab" :class="{active: mode==='purchase'}" @click="setMode('purchase')">进货统计</view>
<view class="mode-tab" :class="{active: mode==='inventory'}" @click="setMode('inventory')">库存统计</view>
<view class="mode-tab" :class="{active: mode==='arap'}" @click="setMode('arap')">应收/应付对账</view>
</view>
<view class="toolbar">
<picker mode="date" :value="startDate" @change="onStartChange"><view class="date">{{ startDate }}</view></picker>
<text style="margin: 0 8rpx;"></text>
<picker mode="date" :value="endDate" @change="onEndChange"><view class="date">{{ endDate }}</view></picker>
</view>
<view class="tabs" v-if="mode==='sale'">
<view class="tab" :class="{active: dim==='customer'}" @click="dim='customer'; refresh()">按客户</view>
<view class="tab" :class="{active: dim==='product'}" @click="dim='product'; refresh()">按货品</view>
</view>
<view class="tabs" v-else-if="mode==='purchase'">
<view class="tab" :class="{active: dim==='supplier'}" @click="dim='supplier'; refresh()">按供应商</view>
<view class="tab" :class="{active: dim==='product'}" @click="dim='product'; refresh()">按货品</view>
</view>
<view class="tabs" v-else-if="mode==='inventory'">
<view class="tab" :class="{active: dim==='qty'}" @click="dim='qty'; refresh()">按数量</view>
<view class="tab" :class="{active: dim==='amount'}" @click="dim='amount'; refresh()">按金额</view>
</view>
<view class="tabs" v-else-if="mode==='arap'">
<view class="tab" :class="{active: dim==='ar'}" @click="dim='ar'; refresh()">应收对账</view>
<view class="tab" :class="{active: dim==='ap'}" @click="dim='ap'; refresh()">应付对账</view>
</view>
<view class="summary">
<view class="item"><text class="label">销售额</text><text class="value"> {{ fmt(total.sales) }}</text></view>
<view class="item"><text class="label">成本</text><text class="value"> {{ fmt(total.cost) }}</text></view>
<view class="item"><text class="label">利润</text><text class="value"> {{ fmt(total.profit) }}</text></view>
<view class="item"><text class="label">利润率</text><text class="value">{{ profitRate }}</text></view>
</view>
<view v-for="(row, idx) in rows" :key="idx" class="card">
<view class="row-head">
<image v-if="row.avatar" class="thumb" :src="row.avatar" />
<view class="title">{{ row.name }}</view>
</view>
<view class="row-body">
<text>销售额 {{ fmt(row.sales) }}</text>
<text style="margin-left: 18rpx;">成本 {{ fmt(row.cost) }}</text>
<text style="margin-left: 18rpx;">利润 {{ fmt(row.profit) }}</text>
</view>
</view>
</view>
</template>
<script>
import { get } from '../../common/http.js'
function formatDate(d) {
const y = d.getFullYear()
const m = String(d.getMonth()+1).padStart(2, '0')
const day = String(d.getDate()).padStart(2, '0')
return `${y}-${m}-${day}`
}
export default {
data() {
const now = new Date()
const start = new Date(now.getFullYear(), now.getMonth(), 1)
return {
startDate: formatDate(start),
endDate: formatDate(now),
mode: 'sale',
dim: 'customer',
rows: [],
total: { sales: 0, cost: 0, profit: 0 }
}
},
onLoad(query) {
try {
const m = query && query.mode
const d = query && query.dim
if (m) this.mode = m
if (d) this.dim = d
} catch(e){}
this.refresh()
},
computed: {
profitRate() {
const { sales, profit } = this.total
if (!sales) return '0.00%'
return ((profit / sales) * 100).toFixed(2) + '%'
}
},
methods: {
fmt(n) { return Number(n || 0).toFixed(2) },
setMode(m) {
this.mode = m
this.dim = m === 'sale' ? 'customer' : m === 'purchase' ? 'supplier' : m === 'inventory' ? 'qty' : 'ar'
this.refresh()
},
onStartChange(e) { this.startDate = e.detail.value; this.refresh() },
onEndChange(e) { this.endDate = e.detail.value; this.refresh() },
async refresh() {
if (this.mode === 'sale') {
if (this.dim === 'customer') return this.loadByCustomer()
if (this.dim === 'product') return this.loadByProduct()
}
if (this.mode === 'purchase') {
if (this.dim === 'supplier') return this.loadPurchaseBySupplier()
if (this.dim === 'product') return this.loadPurchaseByProduct()
}
if (this.mode === 'inventory') {
if (this.dim === 'qty') return this.loadInventoryByQty()
if (this.dim === 'amount') return this.loadInventoryByAmount()
}
if (this.mode === 'arap') {
if (this.dim === 'ar') return this.loadAR()
if (this.dim === 'ap') return this.loadAP()
}
},
async loadByCustomer() {
// 数据来源:/api/orders?biz=sale&type=out 与 /api/products/{id} 获取成本(近似),或由订单明细返回单价与估算成本
// 当前后端列表返回字段包含 amount、customerName缺少明细成本采用二段法
// 1) 列表聚合销售额2) 如存在 productId 与单位进价可获取成本;暂以 0 成本占位,保留接口演进点。
try {
const listResp = await get('/api/orders', { biz: 'sale', type: 'out', startDate: this.startDate, endDate: this.endDate, page: 1, size: 200 })
const list = (listResp && (listResp.list || listResp)) || []
const map = new Map()
let totalSales = 0
for (const it of list) {
const name = it.customerName || '未知客户'
const amount = Number(it.amount || 0)
totalSales += amount
if (!map.has(name)) map.set(name, { name, sales: 0, cost: 0, profit: 0 })
const row = map.get(name)
row.sales += amount
}
const rows = Array.from(map.values()).map(r => ({ ...r, profit: r.sales - r.cost }))
const total = { sales: totalSales, cost: 0, profit: totalSales }
this.rows = rows
this.total = total
} catch (e) {
uni.showToast({ title: '加载失败', icon: 'none' })
}
},
async loadByProduct() {
try {
const listResp = await get('/api/orders', { biz: 'sale', type: 'out', startDate: this.startDate, endDate: this.endDate, page: 1, size: 200 })
const list = (listResp && (listResp.list || listResp)) || []
// 订单详情接口包含明细,逐单补拉详情聚合(规模较小时可接受;后端如提供汇总接口可替换)
const agg = new Map()
for (const it of list) {
try {
const d = await get(`/api/orders/${it.id}`)
const items = d && d.items || []
for (const m of items) {
const key = String(m.productId || m.name)
if (!agg.has(key)) agg.set(key, { name: m.name || ('#'+key), sales: 0, cost: 0, profit: 0 })
const row = agg.get(key)
const sales = Number(m.amount || 0)
// 近似成本:缺后端返回进价,暂以 0待后端扩展返回 purchasePrice
row.sales += sales
}
} catch(_) {}
}
const rows = Array.from(agg.values()).map(r => ({ ...r, profit: r.sales - r.cost }))
const totalSales = rows.reduce((s, r) => s + r.sales, 0)
this.rows = rows
this.total = { sales: totalSales, cost: 0, profit: totalSales }
} catch (e) {
uni.showToast({ title: '加载失败', icon: 'none' })
}
},
async loadPurchaseBySupplier() {
try {
const listResp = await get('/api/purchase-orders', { startDate: this.startDate, endDate: this.endDate, page: 1, size: 200 })
const list = (listResp && (listResp.list || listResp)) || []
const map = new Map(); let total = 0
for (const it of list) {
const name = it.supplierName || '未知供应商'
const amount = Number(it.amount || 0)
total += amount
if (!map.has(name)) map.set(name, { name, sales: 0, cost: 0, profit: 0 })
const row = map.get(name)
// 在进货统计语境里sales 用来展示“进货额”cost/profit 保持 0
row.sales += amount
}
this.rows = Array.from(map.values())
this.total = { sales: total, cost: 0, profit: 0 }
} catch(e) { uni.showToast({ title: '加载失败', icon: 'none' }) }
},
async loadPurchaseByProduct() {
try {
const listResp = await get('/api/purchase-orders', { startDate: this.startDate, endDate: this.endDate, page: 1, size: 200 })
const list = (listResp && (listResp.list || listResp)) || []
const agg = new Map()
for (const it of list) {
try {
const d = await get(`/api/purchase-orders/${it.id}`)
for (const m of (d?.items || [])) {
const key = String(m.productId || m.name)
if (!agg.has(key)) agg.set(key, { name: m.name || ('#'+key), sales: 0, cost: 0, profit: 0 })
const row = agg.get(key)
row.sales += Number(m.amount || 0) // 这里的 sales 表示“进货额”
}
} catch(_){}
}
const rows = Array.from(agg.values())
const total = rows.reduce((s, r)=> s + r.sales, 0)
this.rows = rows; this.total = { sales: total, cost: 0, profit: 0 }
} catch(e) { uni.showToast({ title: '加载失败', icon: 'none' }) }
},
async loadInventoryByQty() {
try {
const resp = await get('/api/inventories/logs', { startDate: this.startDate, endDate: this.endDate, page: 1, size: 200 })
const list = (resp && (resp.list || resp)) || []
const map = new Map(); let totalQty = 0
for (const it of list) {
const key = it.productId || '未知'
if (!map.has(key)) map.set(key, { name: String(key), sales: 0, cost: 0, profit: 0 })
const row = map.get(key)
const q = Number(it.qtyDelta || 0)
row.sales += q
totalQty += q
}
this.rows = Array.from(map.values())
this.total = { sales: totalQty, cost: 0, profit: 0 }
} catch(e) { uni.showToast({ title: '加载失败', icon: 'none' }) }
},
async loadInventoryByAmount() {
try {
const resp = await get('/api/inventories/logs', { startDate: this.startDate, endDate: this.endDate, page: 1, size: 200 })
const list = (resp && (resp.list || resp)) || []
const map = new Map(); let totalAmt = 0
for (const it of list) {
const key = it.productId || '未知'
if (!map.has(key)) map.set(key, { name: String(key), sales: 0, cost: 0, profit: 0 })
const row = map.get(key)
const a = Number(it.amount || it.amountDelta || 0)
row.sales += a
totalAmt += a
}
this.rows = Array.from(map.values())
this.total = { sales: totalAmt, cost: 0, profit: 0 }
} catch(e) { uni.showToast({ title: '加载失败', icon: 'none' }) }
},
async loadAR() {
// 读取客户列表含 receivable 字段,作为对账口径(期末=期初+增加-收回-抹零);后端如提供期间变动接口再替换
try {
const res = await get('/api/customers', { page: 1, size: 100, debtOnly: false })
const list = Array.isArray(res?.list) ? res.list : (Array.isArray(res) ? res : [])
const rows = list.map(c => ({ name: c.name, sales: Number(c.receivable || 0), cost: 0, profit: 0 }))
const total = rows.reduce((s, r)=> s + r.sales, 0)
this.rows = rows; this.total = { sales: total, cost: 0, profit: 0 }
} catch(e) { uni.showToast({ title: '加载失败', icon: 'none' }) }
},
async loadAP() {
// 供应商暂未返回应付字段先展示总览为0并提示后端扩展遵循“不开假数据”
try {
const res = await get('/api/suppliers', { page: 1, size: 100 })
const list = Array.isArray(res?.list) ? res.list : (Array.isArray(res) ? res : [])
const rows = list.map(s => ({ name: s.name, sales: Number(s.apPayable || 0), cost: 0, profit: 0 }))
const total = rows.reduce((s, r)=> s + r.sales, 0)
this.rows = rows; this.total = { sales: total, cost: 0, profit: 0 }
} catch(e) { uni.showToast({ title: '加载失败', icon: 'none' }) }
}
}
}
</script>
<style>
.report { padding: 20rpx; }
.modes { display: flex; gap: 12rpx; margin-bottom: 14rpx; }
.mode-tab { flex: 1; text-align: center; padding: 16rpx 0; border-radius: 999rpx; background: #f4f4f4; color: #666; border: 1rpx solid #e9e9e9; }
.mode-tab.active { background: #1aad19; color: #fff; border-color: #1aad19; font-weight: 700; }
.toolbar { display: flex; align-items: center; gap: 8rpx; background: #fff; padding: 14rpx 16rpx; border-radius: 12rpx; }
.date { padding: 10rpx 16rpx; border: 1rpx solid #eee; border-radius: 8rpx; }
.tabs { display: flex; gap: 16rpx; margin-top: 14rpx; }
.tab { padding: 12rpx 18rpx; border-radius: 999rpx; background: #f4f4f4; color: #666; }
.tab.active { background: #1aad19; color: #fff; }
.summary { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8rpx; margin-top: 14rpx; }
.summary .item { background: #fff; border-radius: 12rpx; padding: 16rpx; }
.summary .label { font-size: 22rpx; color: #888; }
.summary .value { display: block; margin-top: 8rpx; font-weight: 700; color: #333; }
.card { margin-top: 16rpx; background: #fff; border-radius: 12rpx; padding: 16rpx; }
.row-head { display: flex; align-items: center; gap: 12rpx; }
.thumb { width: 72rpx; height: 72rpx; border-radius: 8rpx; background: #f2f2f2; }
.title { font-size: 28rpx; font-weight: 700; }
.row-body { margin-top: 10rpx; color: #666; }
</style>

View File

@@ -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\r\n\r\n// 规范化 WebSocket 关闭码(仅微信小程序)\r\n// #ifdef MP-WEIXIN\r\nif (typeof uni !== 'undefined' && typeof uni.connectSocket === 'function') {\r\n const _connectSocket = uni.connectSocket\r\n uni.connectSocket = function(options) {\r\n const task = _connectSocket.call(this, options)\r\n if (task && typeof task.close === 'function') {\r\n const _close = task.close\r\n task.close = function(params = {}) {\r\n if (params && typeof params === 'object') {\r\n const codeNum = Number(params.code)\r\n const isValid = codeNum === 1000 || (codeNum >= 3000 && codeNum <= 4999)\r\n if (!isValid) {\r\n params.code = 1000\r\n if (!params.reason) params.reason = 'normalized from invalid close code'\r\n }\r\n }\r\n return _close.call(this, params)\r\n }\r\n }\r\n return task\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;AAKA,IAAI,OAAOF,cAAG,UAAK,eAAe,OAAOA,cAAAA,MAAI,kBAAkB,YAAY;AACzE,QAAM,iBAAiBA,cAAAA,MAAI;AAC3BA,sBAAI,gBAAgB,SAAS,SAAS;AACpC,UAAM,OAAO,eAAe,KAAK,MAAM,OAAO;AAC9C,QAAI,QAAQ,OAAO,KAAK,UAAU,YAAY;AAC5C,YAAM,SAAS,KAAK;AACpB,WAAK,QAAQ,SAAS,SAAS,IAAI;AACjC,YAAI,UAAU,OAAO,WAAW,UAAU;AACxC,gBAAM,UAAU,OAAO,OAAO,IAAI;AAClC,gBAAM,UAAU,YAAY,OAAS,WAAW,OAAQ,WAAW;AACnE,cAAI,CAAC,SAAS;AACZ,mBAAO,OAAO;AACd,gBAAI,CAAC,OAAO;AAAQ,qBAAO,SAAS;AAAA,UACrC;AAAA,QACF;AACD,eAAO,OAAO,KAAK,MAAM,MAAM;AAAA,MAChC;AAAA,IACF;AACD,WAAO;AAAA,EACR;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\r\n\r\n// 规范化 WebSocket 关闭码(仅微信小程序)\r\n// #ifdef MP-WEIXIN\r\nif (typeof uni !== 'undefined' && typeof uni.connectSocket === 'function') {\r\n const _connectSocket = uni.connectSocket\r\n uni.connectSocket = function(options) {\r\n const task = _connectSocket.call(this, options)\r\n if (task && typeof task.close === 'function') {\r\n const _close = task.close\r\n task.close = function(params = {}) {\r\n if (params && typeof params === 'object') {\r\n const codeNum = Number(params.code)\r\n const isValid = codeNum === 1000 || (codeNum >= 3000 && codeNum <= 4999)\r\n if (!isValid) {\r\n params.code = 1000\r\n if (!params.reason) params.reason = 'normalized from invalid close code'\r\n }\r\n }\r\n return _close.call(this, params)\r\n }\r\n }\r\n return task\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;AAKA,IAAI,OAAOF,cAAG,UAAK,eAAe,OAAOA,cAAAA,MAAI,kBAAkB,YAAY;AACzE,QAAM,iBAAiBA,cAAAA,MAAI;AAC3BA,sBAAI,gBAAgB,SAAS,SAAS;AACpC,UAAM,OAAO,eAAe,KAAK,MAAM,OAAO;AAC9C,QAAI,QAAQ,OAAO,KAAK,UAAU,YAAY;AAC5C,YAAM,SAAS,KAAK;AACpB,WAAK,QAAQ,SAAS,SAAS,IAAI;AACjC,YAAI,UAAU,OAAO,WAAW,UAAU;AACxC,gBAAM,UAAU,OAAO,OAAO,IAAI;AAClC,gBAAM,UAAU,YAAY,OAAS,WAAW,OAAQ,WAAW;AACnE,cAAI,CAAC,SAAS;AACZ,mBAAO,OAAO;AACd,gBAAI,CAAC,OAAO;AAAQ,qBAAO,SAAS;AAAA,UACrC;AAAA,QACF;AACD,eAAO,OAAO,KAAK,MAAM,MAAM;AAAA,MAChC;AAAA,IACF;AACD,WAAO;AAAA,EACR;AACH;;;"}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -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\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\t<view class=\"fab\" @click=\"create\"></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: [], mode: 'view' } },\r\n\t\tasync onLoad(q) {\r\n\t\t\tthis.mode = (q && q.mode) || 'view'\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\tif (this.mode === 'pick') {\r\n\t\t\t\t\tconst opener = getCurrentPages()[getCurrentPages().length-2]\r\n\t\t\t\t\tif (opener && opener.$vm) {\r\n\t\t\t\t\t\topener.$vm.selectedAccountId = a.id\r\n\t\t\t\t\t\topener.$vm.selectedAccountName = a.name\r\n\t\t\t\t\t}\r\n\t\t\t\t\tuni.navigateBack()\r\n\t\t\t\t} else {\r\n\t\t\t\t\tuni.navigateTo({ url: `/pages/account/ledger?id=${a.id}` })\r\n\t\t\t\t}\r\n\t\t\t},\r\n\t\t\tcreate() { uni.navigateTo({ url: '/pages/account/form' }) },\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\t.fab { position: fixed; right: 32rpx; bottom: 120rpx; width: 100rpx; height: 100rpx; border-radius: 50%; background:#3c9cff; color:#fff; display:flex; align-items:center; justify-content:center; font-size: 52rpx; box-shadow: 0 10rpx 20rpx rgba(0,0,0,0.18); }\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":";;;AAcC,MAAM,WAAW,EAAE,MAAM,MAAM,MAAM,MAAM,QAAQ,OAAO,QAAQ,MAAM,OAAO,KAAK;AACpF,MAAK,YAAU;AAAA,EACd,OAAO;AAAE,WAAO,EAAE,UAAU,CAAA,GAAI,MAAM;EAAU;AAAA,EAChD,MAAM,OAAO,GAAG;AACf,SAAK,OAAQ,KAAK,EAAE,QAAS;AAC7B,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,UAAI,KAAK,SAAS,QAAQ;AACzB,cAAM,SAAS,gBAAiB,EAAC,gBAAe,EAAG,SAAO,CAAC;AAC3D,YAAI,UAAU,OAAO,KAAK;AACzB,iBAAO,IAAI,oBAAoB,EAAE;AACjC,iBAAO,IAAI,sBAAsB,EAAE;AAAA,QACpC;AACAA,sBAAAA,MAAI,aAAa;AAAA,aACX;AACNA,4BAAI,WAAW,EAAE,KAAK,4BAA4B,EAAE,EAAE,IAAI;AAAA,MAC3D;AAAA,IACA;AAAA,IACD,SAAS;AAAEA,oBAAAA,MAAI,WAAW,EAAE,KAAK,sBAAoB,CAAG;AAAA,IAAG;AAAA,IAC3D,UAAU,GAAG;AAAE,aAAO,SAAS,CAAC,KAAK;AAAA,IAAE;AAAA,EACxC;AACD;;;;;;;;;;;;;;;;;ACvCD,GAAG,WAAW,eAAe;"}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
{"version":3,"file":"about.js","sources":["pages/my/about.vue","../../../../Downloads/HBuilderX.4.76.2025082103/HBuilderX/plugins/uniapp-cli-vite/uniPage:/cGFnZXMvbXkvYWJvdXQudnVl"],"sourcesContent":["<template>\r\n\t<view class=\"about\">\r\n\t\t<view class=\"hero\">\r\n\t\t\t<image class=\"logo\" src=\"/static/logo.png\" mode=\"aspectFit\" />\r\n\t\t\t<text class=\"title\">五金配件管家</text>\r\n\t\t\t<text class=\"subtitle\">专注小微门店的极简进销存</text>\r\n\t\t</view>\r\n\r\n\t\t<view class=\"card\">\r\n\t\t\t<view class=\"row\">\r\n\t\t\t\t<text class=\"label\">版本</text>\r\n\t\t\t\t<text class=\"value\">1.0.0</text>\r\n\t\t\t</view>\r\n\t\t\t<view class=\"row\">\r\n\t\t\t\t<text class=\"label\">隐私协议</text>\r\n\t\t\t\t<text class=\"link\" @click=\"openPolicy\">查看</text>\r\n\t\t\t</view>\r\n\t\t\t<view class=\"row\">\r\n\t\t\t\t<text class=\"label\">用户协议</text>\r\n\t\t\t\t<text class=\"link\" @click=\"openTerms\">查看</text>\r\n\t\t\t</view>\r\n\t\t\t<view class=\"row\">\r\n\t\t\t\t<text class=\"label\">个人信息安全投诉</text>\r\n\t\t\t\t<text class=\"link\" @click=\"openComplaint\">提交</text>\r\n\t\t\t</view>\r\n\t\t</view>\r\n\t</view>\r\n</template>\r\n\r\n<script>\r\nexport default {\r\n\tmethods: {\r\n\t\topenPolicy() {\r\n\t\t\tuni.showModal({ title: '隐私协议', content: '隐私协议(静态占位)', showCancel: false })\r\n\t\t},\r\n\t\topenTerms() {\r\n\t\t\tuni.showModal({ title: '用户协议', content: '用户协议(静态占位)', showCancel: false })\r\n\t\t},\r\n\t\topenComplaint() {\r\n\t\t\tuni.showToast({ title: '暂未开通', icon: 'none' })\r\n\t\t}\r\n\t}\r\n}\r\n</script>\r\n\r\n<style>\r\n.about { padding: 24rpx; }\r\n.hero { padding: 32rpx 24rpx; display: flex; flex-direction: column; align-items: center; gap: 10rpx; }\r\n.logo { width: 160rpx; height: 160rpx; border-radius: 32rpx; }\r\n.title { margin-top: 8rpx; font-size: 36rpx; font-weight: 800; color: #333; }\r\n.subtitle { font-size: 26rpx; color: #888; }\r\n.card { margin-top: 18rpx; background: #fff; border-radius: 16rpx; overflow: hidden; }\r\n.row { display: flex; align-items: center; padding: 24rpx; border-top: 1rpx solid #f2f2f2; }\r\n.label { color: #666; }\r\n.value { margin-left: auto; color: #333; }\r\n.link { margin-left: auto; color: #1aad19; }\r\n</style>\r\n\r\n\r\n","import MiniProgramPage from 'C:/Users/21826/Desktop/Wj/PartsInquiry/frontend/pages/my/about.vue'\nwx.createPage(MiniProgramPage)"],"names":["uni"],"mappings":";;;AA8BA,MAAK,YAAU;AAAA,EACd,SAAS;AAAA,IACR,aAAa;AACZA,0BAAI,UAAU,EAAE,OAAO,QAAQ,SAAS,cAAc,YAAY,OAAO;AAAA,IACzE;AAAA,IACD,YAAY;AACXA,0BAAI,UAAU,EAAE,OAAO,QAAQ,SAAS,cAAc,YAAY,OAAO;AAAA,IACzE;AAAA,IACD,gBAAgB;AACfA,oBAAG,MAAC,UAAU,EAAE,OAAO,QAAQ,MAAM,QAAQ;AAAA,IAC9C;AAAA,EACD;AACD;;;;;;;;;;ACzCA,GAAG,WAAW,eAAe;"}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
{"version":3,"file":"entry.js","sources":["pages/report/entry.vue","../../../../Downloads/HBuilderX.4.76.2025082103/HBuilderX/plugins/uniapp-cli-vite/uniPage:/cGFnZXMvcmVwb3J0L2VudHJ5LnZ1ZQ"],"sourcesContent":["<template>\r\n\t<view class=\"entry\">\r\n\t\t<view class=\"section\">\r\n\t\t\t<view class=\"section-title\">资金报表</view>\r\n\t\t\t<view class=\"grid\">\r\n\t\t\t\t<view class=\"btn\" @click=\"go('sale','customer')\">利润统计</view>\r\n\t\t\t\t<view class=\"btn\" @click=\"go('sale','product')\">营业员统计</view>\r\n\t\t\t\t<view class=\"btn\" @click=\"go('sale','customer')\">经营业绩</view>\r\n\t\t\t</view>\r\n\t\t</view>\r\n\t\t<view class=\"section\">\r\n\t\t\t<view class=\"section-title\">进销存报表</view>\r\n\t\t\t<view class=\"grid\">\r\n\t\t\t\t<view class=\"btn\" @click=\"go('sale','customer')\">销售统计</view>\r\n\t\t\t\t<view class=\"btn\" @click=\"go('purchase','supplier')\">进货统计</view>\r\n\t\t\t\t<view class=\"btn\" @click=\"go('inventory','qty')\">库存统计</view>\r\n\t\t\t\t<view class=\"btn\" @click=\"go('arap','ar')\">应收对账单</view>\r\n\t\t\t\t<view class=\"btn\" @click=\"go('arap','ap')\">应付对账单</view>\r\n\t\t\t</view>\r\n\t\t</view>\r\n\t</view>\r\n</template>\r\n\r\n<script>\r\nexport default {\r\n\tmethods: {\r\n\t\tgo(mode, dim) {\r\n\t\t\tconst q = `mode=${encodeURIComponent(mode)}&dim=${encodeURIComponent(dim||'')}`\r\n\t\t\tuni.navigateTo({ url: `/pages/report/index?${q}` })\r\n\t\t}\r\n\t}\r\n}\r\n</script>\r\n\r\n<style>\r\n.entry { padding: 20rpx; }\r\n.section { margin-bottom: 24rpx; }\r\n.section-title { background:#f1f4f8; color:#6a7a8a; padding: 14rpx 16rpx; border-radius: 12rpx; font-weight: 700; }\r\n.grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 18rpx; padding: 18rpx 6rpx 0; }\r\n.btn { text-align: center; padding: 18rpx 8rpx; border: 1rpx solid #e5e9ef; border-radius: 12rpx; color:#333; background: #fff; }\r\n.btn:active { background: #f6f8fa; }\r\n</style>\r\n\r\n\r\n","import MiniProgramPage from 'C:/Users/21826/Desktop/Wj/PartsInquiry/frontend/pages/report/entry.vue'\nwx.createPage(MiniProgramPage)"],"names":["uni"],"mappings":";;AAwBA,MAAK,YAAU;AAAA,EACd,SAAS;AAAA,IACR,GAAG,MAAM,KAAK;AACb,YAAM,IAAI,QAAQ,mBAAmB,IAAI,CAAC,QAAQ,mBAAmB,OAAK,EAAE,CAAC;AAC7EA,oBAAG,MAAC,WAAW,EAAE,KAAK,uBAAuB,CAAC,IAAI;AAAA,IACnD;AAAA,EACD;AACD;;;;;;;;;;;;;;AC9BA,GAAG,WAAW,eAAe;"}

File diff suppressed because one or more lines are too long

View File

@@ -16,7 +16,13 @@ if (!Math) {
"./pages/supplier/select.js";
"./pages/supplier/form.js";
"./pages/account/select.js";
"./pages/account/ledger.js";
"./pages/account/form.js";
"./pages/detail/index.js";
"./pages/my/index.js";
"./pages/my/about.js";
"./pages/report/index.js";
"./pages/report/entry.js";
}
const _sfc_main = {
onLaunch: function() {

View File

@@ -14,7 +14,13 @@
"pages/supplier/select",
"pages/supplier/form",
"pages/account/select",
"pages/detail/index"
"pages/account/ledger",
"pages/account/form",
"pages/detail/index",
"pages/my/index",
"pages/my/about",
"pages/report/index",
"pages/report/entry"
],
"window": {
"navigationBarTextStyle": "black",

View File

@@ -5219,6 +5219,10 @@ function vFor(source, renderItem) {
}
return ret;
}
function setRef(ref2, id, opts = {}) {
const { $templateRefs } = getCurrentInstance();
$templateRefs.push({ i: id, r: ref2, k: opts.k, f: opts.f });
}
function withModelModifiers(fn, { number, trim }, isComponent = false) {
if (isComponent) {
return (...args) => {
@@ -5247,6 +5251,7 @@ const e = (target, ...sources) => extend(target, ...sources);
const n = (value) => normalizeClass(value);
const t = (val) => toDisplayString(val);
const p = (props) => renderProps(props);
const sr = (ref2, id, opts) => setRef(ref2, id, opts);
const m = (fn, modifiers, isComponent = false) => withModelModifiers(fn, modifiers, isComponent);
function createApp$1(rootComponent, rootProps = null) {
rootComponent && (rootComponent.mpType = "app");
@@ -7069,7 +7074,7 @@ function isConsoleWritable() {
function initRuntimeSocketService() {
const hosts = "198.18.0.1,192.168.31.192,127.0.0.1";
const port = "8090";
const id = "mp-weixin_eSBEHk";
const id = "mp-weixin_HpGDB1";
const lazy = typeof swan !== "undefined";
let restoreError = lazy ? () => {
} : initOnError();
@@ -8026,5 +8031,6 @@ exports.o = o;
exports.p = p;
exports.resolveComponent = resolveComponent;
exports.s = s;
exports.sr = sr;
exports.t = t;
//# sourceMappingURL=../../.sourcemap/mp-weixin/common/vendor.js.map

View File

@@ -0,0 +1,102 @@
"use strict";
const common_vendor = require("../../common/vendor.js");
const common_http = require("../../common/http.js");
const _sfc_main = {
data() {
return {
id: null,
form: { name: "", type: "cash", bankName: "", bankAccount: "", openingBalance: "" },
showType: false,
types: [
{ key: "cash", name: "现金" },
{ key: "bank", name: "银行存款" },
{ key: "wechat", name: "微信" },
{ key: "alipay", name: "支付宝" },
{ key: "other", name: "其他" }
]
};
},
onLoad(q) {
this.id = q && q.id ? Number(q.id) : null;
if (this.id)
this.load();
},
methods: {
typeLabel(t) {
const m = { cash: "现金", bank: "银行存款", wechat: "微信", alipay: "支付宝", other: "其他" };
return m[t] || t;
},
async load() {
try {
const list = await common_http.get("/api/accounts");
const a = (Array.isArray(list) ? list : (list == null ? void 0 : list.list) || []).find((x) => x.id == this.id);
if (a) {
this.form = { name: a.name, type: a.type, bankName: a.bank_name || a.bankName || "", bankAccount: a.bank_account || a.bankAccount || "", openingBalance: "" };
}
} catch (e) {
}
},
async save() {
if (!this.form.name) {
common_vendor.index.showToast({ title: "请输入名称", icon: "none" });
return;
}
try {
const body = { ...this.form, openingBalance: Number(this.form.openingBalance || 0) };
if (this.id)
await common_http.put(`/api/accounts/${this.id}`, body);
else
await common_http.post("/api/accounts", { ...body, status: 1 });
common_vendor.index.showToast({ title: "已保存", icon: "success" });
setTimeout(() => common_vendor.index.navigateBack(), 300);
} catch (e) {
common_vendor.index.showToast({ title: "保存失败", icon: "none" });
}
}
}
};
if (!Array) {
const _component_uni_popup = common_vendor.resolveComponent("uni-popup");
_component_uni_popup();
}
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
return common_vendor.e({
a: $data.form.name,
b: common_vendor.o(($event) => $data.form.name = $event.detail.value),
c: common_vendor.t($options.typeLabel($data.form.type)),
d: common_vendor.o(($event) => $data.showType = true),
e: $data.form.type === "bank"
}, $data.form.type === "bank" ? {
f: $data.form.bankName,
g: common_vendor.o(($event) => $data.form.bankName = $event.detail.value)
} : {}, {
h: $data.form.type === "bank"
}, $data.form.type === "bank" ? {
i: $data.form.bankAccount,
j: common_vendor.o(($event) => $data.form.bankAccount = $event.detail.value)
} : {}, {
k: $data.form.openingBalance,
l: common_vendor.o(($event) => $data.form.openingBalance = $event.detail.value),
m: common_vendor.o((...args) => $options.save && $options.save(...args)),
n: common_vendor.f($data.types, (t, k0, i0) => {
return {
a: common_vendor.t(t.name),
b: t.key,
c: common_vendor.o(($event) => {
$data.form.type = t.key;
$data.showType = false;
}, t.key)
};
}),
o: common_vendor.o(($event) => $data.showType = false),
p: common_vendor.sr("popup", "4430e2e8-0"),
q: common_vendor.o(($event) => $data.showType = $event),
r: common_vendor.p({
type: "bottom",
modelValue: $data.showType
})
});
}
const MiniProgramPage = /* @__PURE__ */ common_vendor._export_sfc(_sfc_main, [["render", _sfc_render]]);
wx.createPage(MiniProgramPage);
//# sourceMappingURL=../../../.sourcemap/mp-weixin/pages/account/form.js.map

View File

@@ -0,0 +1,4 @@
{
"navigationBarTitleText": "新增/编辑账户",
"usingComponents": {}
}

View File

@@ -0,0 +1 @@
<view class="page"><view class="form"><view class="field"><text class="label">账户名称</text><input class="input" placeholder="必填" value="{{a}}" bindinput="{{b}}"/></view><view class="field" bindtap="{{d}}"><text class="label">账户类型</text><text class="value">{{c}}</text></view><view wx:if="{{e}}" class="field"><text class="label">银行名称</text><input class="input" placeholder="选填" value="{{f}}" bindinput="{{g}}"/></view><view wx:if="{{h}}" class="field"><text class="label">银行账号</text><input class="input" placeholder="选填" value="{{i}}" bindinput="{{j}}"/></view><view class="field"><text class="label">当前余额</text><input class="input" type="number" placeholder="0.00" value="{{k}}" bindinput="{{l}}"/></view></view><view class="actions"><button class="primary" bindtap="{{m}}">保存</button></view><uni-popup wx:if="{{r}}" class="r" u-s="{{['d']}}" u-r="popup" u-i="4430e2e8-0" bind:__l="__l" bindupdateModelValue="{{q}}" u-p="{{r}}"><view class="sheet"><view wx:for="{{n}}" wx:for-item="t" wx:key="b" class="sheet-item" bindtap="{{t.c}}">{{t.a}}</view><view class="sheet-cancel" bindtap="{{o}}">取消</view></view></uni-popup></view>

View File

@@ -0,0 +1,23 @@
.page { display:flex; flex-direction: column; height: 100vh;
}
.form { background:#fff;
}
.field { display:flex; align-items:center; justify-content: space-between; padding: 18rpx 20rpx; border-bottom:1rpx solid #f3f3f3;
}
.label { color:#666;
}
.input { flex:1; text-align: right; color:#333;
}
.value { color:#333;
}
.actions { margin-top: 20rpx; padding: 0 20rpx;
}
.primary { width: 100%; background: #3c9cff; color:#fff; border-radius: 8rpx; padding: 22rpx 0;
}
.sheet { background:#fff;
}
.sheet-item { padding: 26rpx; text-align:center; border-bottom:1rpx solid #f2f2f2;
}
.sheet-cancel { padding: 26rpx; text-align:center; color:#666;
}

View File

@@ -0,0 +1,83 @@
"use strict";
const common_vendor = require("../../common/vendor.js");
const common_http = require("../../common/http.js");
const _sfc_main = {
data() {
return { accountId: null, startDate: "", endDate: "", list: [], opening: 0, income: 0, expense: 0, ending: 0 };
},
onLoad(query) {
this.accountId = Number(query && query.id);
this.quickInit();
this.load();
},
methods: {
quickInit() {
const now = /* @__PURE__ */ new Date();
const y = now.getFullYear(), m = now.getMonth() + 1;
this.startDate = `${y}-${String(m).padStart(2, "0")}-01`;
const lastDay = new Date(y, m, 0).getDate();
this.endDate = `${y}-${String(m).padStart(2, "0")}-${String(lastDay).padStart(2, "0")}`;
},
async load(page = 1, size = 50) {
try {
const res = await common_http.get(`/api/accounts/${this.accountId}/ledger`, { startDate: this.startDate, endDate: this.endDate, page, size });
this.list = res && res.list || [];
this.opening = Number(res && res.opening || 0);
this.income = Number(res && res.income || 0);
this.expense = Number(res && res.expense || 0);
this.ending = Number(res && res.ending || 0);
} catch (e) {
common_vendor.index.showToast({ title: "加载失败", icon: "none" });
}
},
fmt(v) {
return (typeof v === "number" ? v : Number(v || 0)).toFixed(2);
},
formatDate(s) {
if (!s)
return "-";
try {
const d = new Date(s);
const pad = (n) => String(n).padStart(2, "0");
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
} catch (e) {
return s;
}
}
}
};
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
return {
a: common_vendor.t($data.startDate || "—"),
b: $data.startDate,
c: common_vendor.o((e) => {
$data.startDate = e.detail.value;
$options.load();
}),
d: common_vendor.t($data.endDate || "—"),
e: $data.endDate,
f: common_vendor.o((e) => {
$data.endDate = e.detail.value;
$options.load();
}),
g: common_vendor.t($options.fmt($data.income)),
h: common_vendor.t($options.fmt($data.expense)),
i: common_vendor.t($options.fmt($data.opening)),
j: common_vendor.t($options.fmt($data.ending)),
k: common_vendor.f($data.list, (it, k0, i0) => {
return {
a: common_vendor.t(it.src === "other" ? it.category || "其他" : it.remark || "收付款"),
b: common_vendor.t(it.direction === "in" ? "+" : "-"),
c: common_vendor.t($options.fmt(it.amount)),
d: it.direction === "in" ? 1 : "",
e: it.direction === "out" ? 1 : "",
f: common_vendor.t($options.formatDate(it.tx_time || it.txTime)),
g: common_vendor.t(it.remark || "-"),
h: it.id
};
})
};
}
const MiniProgramPage = /* @__PURE__ */ common_vendor._export_sfc(_sfc_main, [["render", _sfc_render]]);
wx.createPage(MiniProgramPage);
//# sourceMappingURL=../../../.sourcemap/mp-weixin/pages/account/ledger.js.map

View File

@@ -0,0 +1,4 @@
{
"navigationBarTitleText": "账户流水",
"usingComponents": {}
}

View File

@@ -0,0 +1 @@
<view class="page"><view class="filters"><picker mode="date" value="{{b}}" bindchange="{{c}}"><view class="field"><text class="label">开始</text><text class="value">{{a}}</text></view></picker><picker mode="date" value="{{e}}" bindchange="{{f}}"><view class="field"><text class="label">结束</text><text class="value">{{d}}</text></view></picker></view><view class="summary"><view class="sum-item"><text class="k">收入</text><text class="v">{{g}}</text></view><view class="sum-item"><text class="k">支出</text><text class="v">{{h}}</text></view><view class="sum-item"><text class="k">期初</text><text class="v">{{i}}</text></view><view class="sum-item"><text class="k">期末</text><text class="v">{{j}}</text></view></view><scroll-view scroll-y class="list"><view wx:for="{{k}}" wx:for-item="it" wx:key="h" class="item"><view class="row"><text class="title">{{it.a}}</text><text class="{{['amount', it.d && 'in', it.e && 'out']}}">{{it.b}}{{it.c}}</text></view><view class="meta">{{it.f}} · {{it.g}}</view></view></scroll-view></view>

View File

@@ -0,0 +1,35 @@
.page { display:flex; flex-direction: column; height: 100vh;
}
.filters { display:flex; gap: 16rpx; padding: 16rpx; background:#fff;
}
.field { display:flex; justify-content: space-between; align-items:center; padding: 16rpx; border:1rpx solid #eee; border-radius: 12rpx; min-width: 300rpx;
}
.label { color:#666;
}
.value { color:#333;
}
.summary { display:grid; grid-template-columns: repeat(4,1fr); gap: 12rpx; padding: 12rpx 16rpx; background:#fff; border-top:1rpx solid #f1f1f1; border-bottom:1rpx solid #f1f1f1;
}
.sum-item { padding: 12rpx; text-align:center;
}
.k { display:block; color:#888; font-size: 24rpx;
}
.v { display:block; margin-top:6rpx; font-weight:700; color:#333;
}
.list { flex:1;
}
.item { padding: 18rpx 16rpx; border-bottom:1rpx solid #f4f4f4; background:#fff;
}
.row { display:flex; align-items:center; justify-content: space-between; margin-bottom: 6rpx;
}
.title { color:#333;
}
.amount { font-weight:700;
}
.amount.in { color:#2a9d8f;
}
.amount.out { color:#d35b5b;
}
.meta { color:#999; font-size: 24rpx;
}

View File

@@ -4,9 +4,10 @@ const common_http = require("../../common/http.js");
const TYPE_MAP = { cash: "现金", bank: "银行", alipay: "支付宝", wechat: "微信", other: "其他" };
const _sfc_main = {
data() {
return { accounts: [] };
return { accounts: [], mode: "view" };
},
async onLoad() {
async onLoad(q) {
this.mode = q && q.mode || "view";
try {
const res = await common_http.get("/api/accounts");
this.accounts = Array.isArray(res) ? res : (res == null ? void 0 : res.list) || [];
@@ -16,12 +17,19 @@ const _sfc_main = {
},
methods: {
select(a) {
if (this.mode === "pick") {
const opener = getCurrentPages()[getCurrentPages().length - 2];
if (opener && opener.$vm) {
opener.$vm.selectedAccountId = a.id;
opener.$vm.selectedAccountName = a.name;
}
common_vendor.index.navigateBack();
} else {
common_vendor.index.navigateTo({ url: `/pages/account/ledger?id=${a.id}` });
}
},
create() {
common_vendor.index.navigateTo({ url: "/pages/account/form" });
},
typeLabel(t) {
return TYPE_MAP[t] || t;
@@ -39,7 +47,8 @@ function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
d: a.id,
e: common_vendor.o(($event) => $options.select(a), a.id)
};
})
}),
b: common_vendor.o((...args) => $options.create && $options.create(...args))
};
}
const MiniProgramPage = /* @__PURE__ */ common_vendor._export_sfc(_sfc_main, [["render", _sfc_render]]);

View File

@@ -1 +1 @@
<view class="page"><scroll-view scroll-y class="list"><view wx:for="{{a}}" wx:for-item="a" wx:key="d" class="item" bindtap="{{a.e}}"><view class="name">{{a.a}}</view><view class="meta">{{a.b}} · 余额:{{a.c}}</view></view></scroll-view></view>
<view class="page"><scroll-view scroll-y class="list"><view wx:for="{{a}}" wx:for-item="a" wx:key="d" class="item" bindtap="{{a.e}}"><view class="name">{{a.a}}</view><view class="meta">{{a.b}} · 余额:{{a.c}}</view></view></scroll-view><view class="fab" bindtap="{{b}}"></view></view>

View File

@@ -9,3 +9,5 @@
}
.meta { color:#888; font-size: 24rpx;
}
.fab { position: fixed; right: 32rpx; bottom: 120rpx; width: 100rpx; height: 100rpx; border-radius: 50%; background:#3c9cff; color:#fff; display:flex; align-items:center; justify-content:center; font-size: 52rpx; box-shadow: 0 10rpx 20rpx rgba(0,0,0,0.18);
}

View File

@@ -67,6 +67,10 @@ const _sfc_main = {
common_vendor.index.navigateTo({ url: "/pages/customer/select" });
return;
}
if (item.key === "account") {
common_vendor.index.navigateTo({ url: "/pages/account/select" });
return;
}
if (item.key === "supplier") {
common_vendor.index.navigateTo({ url: "/pages/supplier/select" });
return;
@@ -83,11 +87,19 @@ const _sfc_main = {
goDetail() {
this.activeTab = "detail";
try {
common_vendor.index.__f__("log", "at pages/index/index.vue:174", "[index] goDetail → /pages/detail/index");
common_vendor.index.__f__("log", "at pages/index/index.vue:179", "[index] goDetail → /pages/detail/index");
} catch (e) {
}
common_vendor.index.navigateTo({ url: "/pages/detail/index" });
},
goReport() {
this.activeTab = "report";
common_vendor.index.navigateTo({ url: "/pages/report/entry" });
},
goMe() {
this.activeTab = "me";
common_vendor.index.navigateTo({ url: "/pages/my/index" });
},
onNoticeTap(n) {
common_vendor.index.showModal({
title: "广告",
@@ -148,9 +160,9 @@ function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
q: $data.activeTab === "detail" ? 1 : "",
r: common_vendor.o((...args) => $options.goDetail && $options.goDetail(...args)),
s: $data.activeTab === "report" ? 1 : "",
t: common_vendor.o(($event) => $data.activeTab = "report"),
t: common_vendor.o((...args) => $options.goReport && $options.goReport(...args)),
v: $data.activeTab === "me" ? 1 : "",
w: common_vendor.o(($event) => $data.activeTab = "me")
w: common_vendor.o((...args) => $options.goMe && $options.goMe(...args))
});
}
const MiniProgramPage = /* @__PURE__ */ common_vendor._export_sfc(_sfc_main, [["render", _sfc_render]]);

View File

@@ -0,0 +1,27 @@
"use strict";
const common_vendor = require("../../common/vendor.js");
const common_assets = require("../../common/assets.js");
const _sfc_main = {
methods: {
openPolicy() {
common_vendor.index.showModal({ title: "隐私协议", content: "隐私协议(静态占位)", showCancel: false });
},
openTerms() {
common_vendor.index.showModal({ title: "用户协议", content: "用户协议(静态占位)", showCancel: false });
},
openComplaint() {
common_vendor.index.showToast({ title: "暂未开通", icon: "none" });
}
}
};
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
return {
a: common_assets._imports_0$1,
b: common_vendor.o((...args) => $options.openPolicy && $options.openPolicy(...args)),
c: common_vendor.o((...args) => $options.openTerms && $options.openTerms(...args)),
d: common_vendor.o((...args) => $options.openComplaint && $options.openComplaint(...args))
};
}
const MiniProgramPage = /* @__PURE__ */ common_vendor._export_sfc(_sfc_main, [["render", _sfc_render]]);
wx.createPage(MiniProgramPage);
//# sourceMappingURL=../../../.sourcemap/mp-weixin/pages/my/about.js.map

View File

@@ -0,0 +1,4 @@
{
"navigationBarTitleText": "关于与协议",
"usingComponents": {}
}

View File

@@ -0,0 +1 @@
<view class="about"><view class="hero"><image class="logo" src="{{a}}" mode="aspectFit"/><text class="title">五金配件管家</text><text class="subtitle">专注小微门店的极简进销存</text></view><view class="card"><view class="row"><text class="label">版本</text><text class="value">1.0.0</text></view><view class="row"><text class="label">隐私协议</text><text class="link" bindtap="{{b}}">查看</text></view><view class="row"><text class="label">用户协议</text><text class="link" bindtap="{{c}}">查看</text></view><view class="row"><text class="label">个人信息安全投诉</text><text class="link" bindtap="{{d}}">提交</text></view></view></view>

View File

@@ -0,0 +1,21 @@
.about { padding: 24rpx;
}
.hero { padding: 32rpx 24rpx; display: flex; flex-direction: column; align-items: center; gap: 10rpx;
}
.logo { width: 160rpx; height: 160rpx; border-radius: 32rpx;
}
.title { margin-top: 8rpx; font-size: 36rpx; font-weight: 800; color: #333;
}
.subtitle { font-size: 26rpx; color: #888;
}
.card { margin-top: 18rpx; background: #fff; border-radius: 16rpx; overflow: hidden;
}
.row { display: flex; align-items: center; padding: 24rpx; border-top: 1rpx solid #f2f2f2;
}
.label { color: #666;
}
.value { margin-left: auto; color: #333;
}
.link { margin-left: auto; color: #1aad19;
}

View File

@@ -0,0 +1,110 @@
"use strict";
const common_vendor = require("../../common/vendor.js");
const common_http = require("../../common/http.js");
const _sfc_main = {
data() {
return {
avatarUrl: "/static/logo.png",
shopName: "我的店铺",
mobile: ""
};
},
onLoad() {
this.fetchProfile();
},
computed: {
mobileDisplay() {
const m = String(this.mobile || "");
return m.length === 11 ? m.slice(0, 3) + "****" + m.slice(7) : m || "未绑定手机号";
}
},
methods: {
async fetchProfile() {
try {
await common_http.get("/api/dashboard/overview");
} catch (e) {
}
try {
const storeName = common_vendor.index.getStorageSync("SHOP_NAME") || "";
const avatar = common_vendor.index.getStorageSync("USER_AVATAR") || "";
const phone = common_vendor.index.getStorageSync("USER_MOBILE") || "";
if (storeName)
this.shopName = storeName;
if (avatar)
this.avatarUrl = avatar;
this.mobile = phone;
} catch (e) {
}
},
onAvatarError() {
this.avatarUrl = "/static/logo.png";
},
goVip() {
common_vendor.index.showToast({ title: "VIP会员开发中", icon: "none" });
},
goMyOrders() {
common_vendor.index.navigateTo({ url: "/pages/detail/index" });
},
goSupplier() {
common_vendor.index.navigateTo({ url: "/pages/supplier/select" });
},
goCustomer() {
common_vendor.index.navigateTo({ url: "/pages/customer/select" });
},
goCustomerQuote() {
common_vendor.index.showToast({ title: "客户报价(开发中)", icon: "none" });
},
goShop() {
common_vendor.index.showToast({ title: "店铺管理(开发中)", icon: "none" });
},
editProfile() {
common_vendor.index.showToast({ title: "账号与安全(开发中)", icon: "none" });
},
goProductSettings() {
common_vendor.index.navigateTo({ url: "/pages/product/settings" });
},
goSystemParams() {
common_vendor.index.showToast({ title: "系统参数(开发中)", icon: "none" });
},
goAbout() {
common_vendor.index.navigateTo({ url: "/pages/my/about" });
},
logout() {
try {
common_vendor.index.removeStorageSync("TOKEN");
common_vendor.index.removeStorageSync("USER_AVATAR");
common_vendor.index.removeStorageSync("USER_NAME");
common_vendor.index.removeStorageSync("USER_MOBILE");
common_vendor.index.removeStorageSync("SHOP_NAME");
common_vendor.index.showToast({ title: "已退出", icon: "none" });
setTimeout(() => {
common_vendor.index.reLaunch({ url: "/pages/index/index" });
}, 300);
} catch (e) {
common_vendor.index.reLaunch({ url: "/pages/index/index" });
}
}
}
};
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
return {
a: $data.avatarUrl,
b: common_vendor.o((...args) => $options.onAvatarError && $options.onAvatarError(...args)),
c: common_vendor.t($data.shopName),
d: common_vendor.t($options.mobileDisplay),
e: common_vendor.o((...args) => $options.goVip && $options.goVip(...args)),
f: common_vendor.o((...args) => $options.goMyOrders && $options.goMyOrders(...args)),
g: common_vendor.o((...args) => $options.goSupplier && $options.goSupplier(...args)),
h: common_vendor.o((...args) => $options.goCustomer && $options.goCustomer(...args)),
i: common_vendor.o((...args) => $options.goCustomerQuote && $options.goCustomerQuote(...args)),
j: common_vendor.o((...args) => $options.goShop && $options.goShop(...args)),
k: common_vendor.o((...args) => $options.editProfile && $options.editProfile(...args)),
l: common_vendor.o((...args) => $options.goProductSettings && $options.goProductSettings(...args)),
m: common_vendor.o((...args) => $options.goSystemParams && $options.goSystemParams(...args)),
n: common_vendor.o((...args) => $options.goAbout && $options.goAbout(...args)),
o: common_vendor.o((...args) => $options.logout && $options.logout(...args))
};
}
const MiniProgramPage = /* @__PURE__ */ common_vendor._export_sfc(_sfc_main, [["render", _sfc_render]]);
wx.createPage(MiniProgramPage);
//# sourceMappingURL=../../../.sourcemap/mp-weixin/pages/my/index.js.map

View File

@@ -0,0 +1,4 @@
{
"navigationBarTitleText": "我的",
"usingComponents": {}
}

View File

@@ -0,0 +1 @@
<view class="me"><view class="card user"><image class="avatar" src="{{a}}" mode="aspectFill" binderror="{{b}}"/><view class="meta"><text class="name">{{c}}</text><text class="phone">{{d}}</text><text class="role">老板</text></view></view><view class="group"><view class="group-title">会员与订单</view><view class="cell" bindtap="{{e}}"><text>VIP会员</text><text class="arrow"></text></view><view class="cell" bindtap="{{f}}"><text>我的订单</text><text class="arrow"></text></view></view><view class="group"><view class="group-title">基础管理</view><view class="cell" bindtap="{{g}}"><text>供应商管理</text><text class="arrow"></text></view><view class="cell" bindtap="{{h}}"><text>客户管理</text><text class="arrow"></text></view><view class="cell" bindtap="{{i}}"><text>客户报价</text><text class="arrow"></text></view><view class="cell" bindtap="{{j}}"><text>店铺管理</text><text class="arrow"></text></view></view><view class="group"><view class="group-title">设置中心</view><view class="cell" bindtap="{{k}}"><text>账号与安全</text><text class="desc">修改头像、姓名、密码</text><text class="arrow"></text></view><view class="cell" bindtap="{{l}}"><text>商品设置</text><text class="arrow"></text></view><view class="cell" bindtap="{{m}}"><text>系统参数</text><text class="desc">低价提示、默认收款、单行折扣等</text><text class="arrow"></text></view><view class="cell" bindtap="{{n}}"><text>关于与协议</text><text class="arrow"></text></view><view class="cell danger" bindtap="{{o}}"><text>退出登录</text></view></view></view>

View File

@@ -0,0 +1,27 @@
.me { padding: 24rpx;
}
.card.user { display: flex; gap: 18rpx; padding: 22rpx; background: #fff; border-radius: 16rpx; box-shadow: 0 6rpx 16rpx rgba(0,0,0,0.06); align-items: center;
}
.avatar { width: 120rpx; height: 120rpx; border-radius: 60rpx; background: #f5f5f5;
}
.meta { display: flex; flex-direction: column; gap: 6rpx;
}
.name { font-size: 34rpx; font-weight: 700; color: #333;
}
.phone { font-size: 26rpx; color: #888;
}
.role { font-size: 22rpx; color: #999;
}
.group { margin-top: 24rpx; background: #fff; border-radius: 16rpx; overflow: hidden;
}
.group-title { padding: 18rpx 22rpx; font-size: 26rpx; color: #999; background: #fafafa;
}
.cell { display: flex; align-items: center; padding: 26rpx 22rpx; border-top: 1rpx solid #f0f0f0; color: #333;
}
.cell .desc { margin-left: auto; margin-right: 8rpx; font-size: 22rpx; color: #999;
}
.cell .arrow { margin-left: auto; color: #bbb;
}
.cell.danger { color: #dd524d; justify-content: center; font-weight: 700;
}

View File

@@ -28,6 +28,7 @@ const _sfc_main = {
supplierName: "",
items: [],
activeCategory: "sale_income",
counterpartyType: "customer",
trxAmount: 0,
selectedAccountId: null,
selectedAccountName: "",
@@ -50,16 +51,16 @@ const _sfc_main = {
return this.supplierName || "零散供应商";
},
incomeCategories() {
return common_constants.INCOME_CATEGORIES;
return this._incomeCategories || common_constants.INCOME_CATEGORIES;
},
expenseCategories() {
return common_constants.EXPENSE_CATEGORIES;
return this._expenseCategories || common_constants.EXPENSE_CATEGORIES;
},
accountLabel() {
return this.selectedAccountName || "现金";
},
counterpartyLabel() {
return this.customerName || this.supplierName || "—";
return this.counterpartyType === "customer" ? this.customerName || "—" : this.supplierName || "—";
},
// 收款/付款合计
payTotal() {
@@ -67,6 +68,9 @@ const _sfc_main = {
return Number(p.cash || 0) + Number(p.bank || 0) + Number(p.wechat || 0);
}
},
onLoad() {
this.fetchCategories();
},
onShow() {
if (this.biz === "sale") {
if (this.order.customerId && this.order.customerId !== this._lastCustomerId) {
@@ -85,6 +89,26 @@ const _sfc_main = {
}
},
methods: {
async fetchCategories() {
try {
const res = await common_http.get("/api/finance/categories");
if (res && Array.isArray(res.incomeCategories))
this._incomeCategories = res.incomeCategories;
if (res && Array.isArray(res.expenseCategories))
this._expenseCategories = res.expenseCategories;
this.ensureActiveCategory();
} catch (_) {
this.ensureActiveCategory();
}
},
ensureActiveCategory() {
const list = this.biz === "income" ? this.incomeCategories || [] : this.expenseCategories || [];
if (!list.length)
return;
const exists = list.some((it) => it && it.key === this.activeCategory);
if (!exists)
this.activeCategory = list[0].key;
},
async loadCustomerLevel(customerId) {
try {
const d = await common_http.get(`/api/customers/${customerId}`);
@@ -133,6 +157,7 @@ const _sfc_main = {
},
switchBiz(type) {
this.biz = type;
this.ensureActiveCategory();
},
onDateChange(e) {
this.order.orderTime = e.detail.value;
@@ -147,13 +172,21 @@ const _sfc_main = {
common_vendor.index.navigateTo({ url: "/pages/product/select" });
},
chooseAccount() {
common_vendor.index.navigateTo({ url: "/pages/account/select" });
common_vendor.index.navigateTo({ url: "/pages/account/select?mode=pick" });
},
chooseCounterparty() {
if (this.biz === "income" || this.biz === "expense") {
if (!(this.biz === "income" || this.biz === "expense"))
return;
if (this.counterpartyType === "customer") {
common_vendor.index.navigateTo({ url: "/pages/customer/select" });
} else {
common_vendor.index.navigateTo({ url: "/pages/supplier/select" });
}
},
setCounterparty(t) {
this.counterpartyType = t;
this.ensureActiveCategory();
},
recalc() {
this.$forceUpdate();
},
@@ -189,7 +222,8 @@ const _sfc_main = {
} : {
type: this.biz,
category: this.activeCategory,
counterpartyId: this.order.customerId || null,
counterpartyType: this.counterpartyType,
counterpartyId: this.counterpartyType === "customer" ? this.order.customerId || null : this.order.supplierId || null,
accountId: this.selectedAccountId || null,
amount: Number(this.trxAmount || 0),
txTime: this.order.orderTime,
@@ -288,7 +322,11 @@ function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
ac: common_vendor.t($options.totalAmount.toFixed(2)),
ad: common_vendor.o((...args) => $options.chooseProduct && $options.chooseProduct(...args))
} : {
ae: common_vendor.f($data.biz === "income" ? $options.incomeCategories : $options.expenseCategories, (c, k0, i0) => {
ae: $data.counterpartyType === "customer" ? 1 : "",
af: common_vendor.o(($event) => $options.setCounterparty("customer")),
ag: $data.counterpartyType === "supplier" ? 1 : "",
ah: common_vendor.o(($event) => $options.setCounterparty("supplier")),
ai: common_vendor.f($data.biz === "income" ? $options.incomeCategories : $options.expenseCategories, (c, k0, i0) => {
return {
a: common_vendor.t(c.label),
b: c.key,
@@ -296,23 +334,23 @@ function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
d: common_vendor.o(($event) => $data.activeCategory = c.key, c.key)
};
}),
af: common_vendor.t($options.counterpartyLabel),
ag: common_vendor.o((...args) => $options.chooseCounterparty && $options.chooseCounterparty(...args)),
ah: common_vendor.t($options.accountLabel),
ai: common_vendor.o((...args) => $options.chooseAccount && $options.chooseAccount(...args)),
aj: $data.trxAmount,
ak: common_vendor.o(common_vendor.m(($event) => $data.trxAmount = $event.detail.value, {
aj: common_vendor.t($options.counterpartyLabel),
ak: common_vendor.o((...args) => $options.chooseCounterparty && $options.chooseCounterparty(...args)),
al: common_vendor.t($options.accountLabel),
am: common_vendor.o((...args) => $options.chooseAccount && $options.chooseAccount(...args)),
an: $data.trxAmount,
ao: common_vendor.o(common_vendor.m(($event) => $data.trxAmount = $event.detail.value, {
number: true
})),
al: $data.order.remark,
am: common_vendor.o(($event) => $data.order.remark = $event.detail.value)
ap: $data.order.remark,
aq: common_vendor.o(($event) => $data.order.remark = $event.detail.value)
}, {
aa: $data.biz === "sale" || $data.biz === "purchase",
an: !$data.items.length
ar: !$data.items.length
}, !$data.items.length ? {
ao: common_assets._imports_0$1
as: common_assets._imports_0$1
} : {
ap: common_vendor.f($data.items, (it, idx, i0) => {
at: common_vendor.f($data.items, (it, idx, i0) => {
return {
a: common_vendor.t(it.productName),
b: common_vendor.o([common_vendor.m(($event) => it.quantity = $event.detail.value, {
@@ -328,8 +366,8 @@ function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
};
})
}, {
aq: common_vendor.o((...args) => $options.saveAndReset && $options.saveAndReset(...args)),
ar: common_vendor.o((...args) => $options.submit && $options.submit(...args))
av: common_vendor.o((...args) => $options.saveAndReset && $options.saveAndReset(...args)),
aw: common_vendor.o((...args) => $options.submit && $options.submit(...args))
});
}
const MiniProgramPage = /* @__PURE__ */ common_vendor._export_sfc(_sfc_main, [["render", _sfc_render]]);

View File

@@ -1 +1 @@
<view class="order"><view class="tabs"><text class="{{[a && 'active']}}" bindtap="{{b}}">销售</text><text class="{{[c && 'active']}}" bindtap="{{d}}">进货</text><text class="{{[e && 'active']}}" bindtap="{{f}}">其他收入</text><text class="{{[g && 'active']}}" bindtap="{{h}}">其他支出</text></view><view wx:if="{{i}}" class="subtabs"><button class="{{['subbtn', j && 'active']}}" bindtap="{{k}}">出货</button><button class="{{['subbtn', l && 'active']}}" bindtap="{{m}}">退货</button><button class="{{['subbtn', n && 'active']}}" bindtap="{{o}}">收款</button></view><view wx:elif="{{p}}" class="subtabs"><button class="{{['subbtn', q && 'active']}}" bindtap="{{r}}">进货</button><button class="{{['subbtn', s && 'active']}}" bindtap="{{t}}">退货</button><button class="{{['subbtn', v && 'active']}}" bindtap="{{w}}">付款</button></view><picker mode="date" value="{{y}}" bindchange="{{z}}"><view class="field"><text class="label">时间</text><text class="value">{{x}}</text></view></picker><view wx:if="{{A}}" class="field" bindtap="{{C}}"><text class="label">客户</text><text class="value">{{B}}</text></view><view wx:elif="{{D}}" class="field" bindtap="{{F}}"><text class="label">供应商</text><text class="value">{{E}}</text></view><view wx:if="{{G}}"><view wx:if="{{H}}" class="field" bindtap="{{J}}"><text class="label">客户</text><text class="value">{{I}}</text></view><view wx:else class="field" bindtap="{{L}}"><text class="label">供应商</text><text class="value">{{K}}</text></view><view class="field pay-row"><text class="label">现金</text><input class="pay-input" type="digit" placeholder="0.00" bindinput="{{M}}" value="{{N}}"/></view><view class="field pay-row"><text class="label">银行存款</text><input class="pay-input" type="digit" placeholder="0.00" bindinput="{{O}}" value="{{P}}"/></view><view class="field pay-row"><text class="label">微信</text><input class="pay-input" type="digit" placeholder="0.00" bindinput="{{Q}}" value="{{R}}"/></view><view class="collapse-trigger" bindtap="{{T}}">{{S}}</view><view class="textarea"><view class="amount-badge">总金额:{{U}}</view><block wx:if="{{r0}}"><textarea maxlength="200" placeholder="备注最多输入200个字" value="{{V}}" bindinput="{{W}}"></textarea></block><view class="date-mini"><picker mode="date" value="{{Y}}" bindchange="{{Z}}"><text>{{X}}</text></picker></view></view></view><view wx:elif="{{aa}}"><view class="summary"><text>选中货品({{ab}}</text><text>合计金额:¥ {{ac}}</text></view><view class="add" bindtap="{{ad}}">+</view></view><view wx:else><view class="chips"><view wx:for="{{ae}}" wx:for-item="c" wx:key="b" class="{{['chip', c.c && 'active']}}" bindtap="{{c.d}}">{{c.a}}</view></view><view class="field" bindtap="{{ag}}"><text class="label">往来单位</text><text class="value">{{af}}</text></view><view class="field" bindtap="{{ai}}"><text class="label">结算账户</text><text class="value">{{ah}}</text></view><view class="field"><text class="label">金额</text><input class="value" type="digit" placeholder="0.00" value="{{aj}}" bindinput="{{ak}}"/></view><view class="textarea"><block wx:if="{{r0}}"><textarea maxlength="200" placeholder="备注最多输入200个字" value="{{al}}" bindinput="{{am}}"></textarea></block></view></view><view wx:if="{{an}}" class="empty"><image src="{{ao}}" mode="widthFix" class="empty-img"></image><text class="empty-text">购物车里空空如也</text><text class="empty-sub">扫描或点击 “+” 选择商品吧</text></view><view wx:else class="list"><view wx:for="{{ap}}" wx:for-item="it" wx:key="g" class="row"><view class="col name">{{it.a}}</view><view class="col qty"><input type="number" bindinput="{{it.b}}" value="{{it.c}}"/></view><view class="col price"><input type="number" bindinput="{{it.d}}" value="{{it.e}}"/></view><view class="col amount">¥ {{it.f}}</view></view></view><view class="bottom"><button class="ghost" bindtap="{{aq}}">再记一笔</button><button class="primary" bindtap="{{ar}}">保存</button></view></view>
<view class="order"><view class="tabs"><text class="{{[a && 'active']}}" bindtap="{{b}}">销售</text><text class="{{[c && 'active']}}" bindtap="{{d}}">进货</text><text class="{{[e && 'active']}}" bindtap="{{f}}">其他收入</text><text class="{{[g && 'active']}}" bindtap="{{h}}">其他支出</text></view><view wx:if="{{i}}" class="subtabs"><button class="{{['subbtn', j && 'active']}}" bindtap="{{k}}">出货</button><button class="{{['subbtn', l && 'active']}}" bindtap="{{m}}">退货</button><button class="{{['subbtn', n && 'active']}}" bindtap="{{o}}">收款</button></view><view wx:elif="{{p}}" class="subtabs"><button class="{{['subbtn', q && 'active']}}" bindtap="{{r}}">进货</button><button class="{{['subbtn', s && 'active']}}" bindtap="{{t}}">退货</button><button class="{{['subbtn', v && 'active']}}" bindtap="{{w}}">付款</button></view><picker mode="date" value="{{y}}" bindchange="{{z}}"><view class="field"><text class="label">时间</text><text class="value">{{x}}</text></view></picker><view wx:if="{{A}}" class="field" bindtap="{{C}}"><text class="label">客户</text><text class="value">{{B}}</text></view><view wx:elif="{{D}}" class="field" bindtap="{{F}}"><text class="label">供应商</text><text class="value">{{E}}</text></view><view wx:if="{{G}}"><view wx:if="{{H}}" class="field" bindtap="{{J}}"><text class="label">客户</text><text class="value">{{I}}</text></view><view wx:else class="field" bindtap="{{L}}"><text class="label">供应商</text><text class="value">{{K}}</text></view><view class="field pay-row"><text class="label">现金</text><input class="pay-input" type="digit" placeholder="0.00" bindinput="{{M}}" value="{{N}}"/></view><view class="field pay-row"><text class="label">银行存款</text><input class="pay-input" type="digit" placeholder="0.00" bindinput="{{O}}" value="{{P}}"/></view><view class="field pay-row"><text class="label">微信</text><input class="pay-input" type="digit" placeholder="0.00" bindinput="{{Q}}" value="{{R}}"/></view><view class="collapse-trigger" bindtap="{{T}}">{{S}}</view><view class="textarea"><view class="amount-badge">总金额:{{U}}</view><block wx:if="{{r0}}"><textarea maxlength="200" placeholder="备注最多输入200个字" value="{{V}}" bindinput="{{W}}"></textarea></block><view class="date-mini"><picker mode="date" value="{{Y}}" bindchange="{{Z}}"><text>{{X}}</text></picker></view></view></view><view wx:elif="{{aa}}"><view class="summary"><text>选中货品({{ab}}</text><text>合计金额:¥ {{ac}}</text></view><view class="add" bindtap="{{ad}}">+</view></view><view wx:else><view class="subtabs"><button class="{{['subbtn', ae && 'active']}}" bindtap="{{af}}">客户</button><button class="{{['subbtn', ag && 'active']}}" bindtap="{{ah}}">供应商</button></view><view class="chips"><view wx:for="{{ai}}" wx:for-item="c" wx:key="b" class="{{['chip', c.c && 'active']}}" bindtap="{{c.d}}">{{c.a}}</view></view><view class="field" bindtap="{{ak}}"><text class="label">往来单位</text><text class="value">{{aj}}</text></view><view class="field" bindtap="{{am}}"><text class="label">结算账户</text><text class="value">{{al}}</text></view><view class="field"><text class="label">金额</text><input class="value" type="digit" placeholder="0.00" value="{{an}}" bindinput="{{ao}}"/></view><view class="textarea"><block wx:if="{{r0}}"><textarea maxlength="200" placeholder="备注最多输入200个字" value="{{ap}}" bindinput="{{aq}}"></textarea></block></view></view><view wx:if="{{ar}}" class="empty"><image src="{{as}}" mode="widthFix" class="empty-img"></image><text class="empty-text">购物车里空空如也</text><text class="empty-sub">扫描或点击 “+” 选择商品吧</text></view><view wx:else class="list"><view wx:for="{{at}}" wx:for-item="it" wx:key="g" class="row"><view class="col name">{{it.a}}</view><view class="col qty"><input type="number" bindinput="{{it.b}}" value="{{it.c}}"/></view><view class="col price"><input type="number" bindinput="{{it.d}}" value="{{it.e}}"/></view><view class="col amount">¥ {{it.f}}</view></view></view><view class="bottom"><button class="ghost" bindtap="{{av}}">再记一笔</button><button class="primary" bindtap="{{aw}}">保存</button></view></view>

View File

@@ -49,4 +49,11 @@
.amount-badge { position: absolute; right: 24rpx; top: -36rpx; background: #d1f0ff; color:#107e9b; padding: 8rpx 16rpx; border-radius: 12rpx; font-size: 24rpx;
}
.date-mini { position: absolute; right: 24rpx; bottom: 20rpx; color:#666; font-size: 24rpx;
}
/* 分类chips样式选中后文字变红 */
.chips { display:flex; flex-wrap: wrap; gap: 12rpx; padding: 12rpx 24rpx;
}
.chip { padding: 10rpx 20rpx; border-radius: 999rpx; background: #f4f4f4; color:#666;
}
.chip.active { color: #e54d42;
}

View File

@@ -0,0 +1,25 @@
"use strict";
const common_vendor = require("../../common/vendor.js");
const _sfc_main = {
methods: {
go(mode, dim) {
const q = `mode=${encodeURIComponent(mode)}&dim=${encodeURIComponent(dim || "")}`;
common_vendor.index.navigateTo({ url: `/pages/report/index?${q}` });
}
}
};
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
return {
a: common_vendor.o(($event) => $options.go("sale", "customer")),
b: common_vendor.o(($event) => $options.go("sale", "product")),
c: common_vendor.o(($event) => $options.go("sale", "customer")),
d: common_vendor.o(($event) => $options.go("sale", "customer")),
e: common_vendor.o(($event) => $options.go("purchase", "supplier")),
f: common_vendor.o(($event) => $options.go("inventory", "qty")),
g: common_vendor.o(($event) => $options.go("arap", "ar")),
h: common_vendor.o(($event) => $options.go("arap", "ap"))
};
}
const MiniProgramPage = /* @__PURE__ */ common_vendor._export_sfc(_sfc_main, [["render", _sfc_render]]);
wx.createPage(MiniProgramPage);
//# sourceMappingURL=../../../.sourcemap/mp-weixin/pages/report/entry.js.map

View File

@@ -0,0 +1,4 @@
{
"navigationBarTitleText": "报表",
"usingComponents": {}
}

View File

@@ -0,0 +1 @@
<view class="entry"><view class="section"><view class="section-title">资金报表</view><view class="grid"><view class="btn" bindtap="{{a}}">利润统计</view><view class="btn" bindtap="{{b}}">营业员统计</view><view class="btn" bindtap="{{c}}">经营业绩</view></view></view><view class="section"><view class="section-title">进销存报表</view><view class="grid"><view class="btn" bindtap="{{d}}">销售统计</view><view class="btn" bindtap="{{e}}">进货统计</view><view class="btn" bindtap="{{f}}">库存统计</view><view class="btn" bindtap="{{g}}">应收对账单</view><view class="btn" bindtap="{{h}}">应付对账单</view></view></view></view>

View File

@@ -0,0 +1,13 @@
.entry { padding: 20rpx;
}
.section { margin-bottom: 24rpx;
}
.section-title { background:#f1f4f8; color:#6a7a8a; padding: 14rpx 16rpx; border-radius: 12rpx; font-weight: 700;
}
.grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 18rpx; padding: 18rpx 6rpx 0;
}
.btn { text-align: center; padding: 18rpx 8rpx; border: 1rpx solid #e5e9ef; border-radius: 12rpx; color:#333; background: #fff;
}
.btn:active { background: #f6f8fa;
}

View File

@@ -0,0 +1,338 @@
"use strict";
const common_vendor = require("../../common/vendor.js");
const common_http = require("../../common/http.js");
function formatDate(d) {
const y = d.getFullYear();
const m = String(d.getMonth() + 1).padStart(2, "0");
const day = String(d.getDate()).padStart(2, "0");
return `${y}-${m}-${day}`;
}
const _sfc_main = {
data() {
const now = /* @__PURE__ */ new Date();
const start = new Date(now.getFullYear(), now.getMonth(), 1);
return {
startDate: formatDate(start),
endDate: formatDate(now),
mode: "sale",
dim: "customer",
rows: [],
total: { sales: 0, cost: 0, profit: 0 }
};
},
onLoad(query) {
try {
const m = query && query.mode;
const d = query && query.dim;
if (m)
this.mode = m;
if (d)
this.dim = d;
} catch (e) {
}
this.refresh();
},
computed: {
profitRate() {
const { sales, profit } = this.total;
if (!sales)
return "0.00%";
return (profit / sales * 100).toFixed(2) + "%";
}
},
methods: {
fmt(n) {
return Number(n || 0).toFixed(2);
},
setMode(m) {
this.mode = m;
this.dim = m === "sale" ? "customer" : m === "purchase" ? "supplier" : m === "inventory" ? "qty" : "ar";
this.refresh();
},
onStartChange(e) {
this.startDate = e.detail.value;
this.refresh();
},
onEndChange(e) {
this.endDate = e.detail.value;
this.refresh();
},
async refresh() {
if (this.mode === "sale") {
if (this.dim === "customer")
return this.loadByCustomer();
if (this.dim === "product")
return this.loadByProduct();
}
if (this.mode === "purchase") {
if (this.dim === "supplier")
return this.loadPurchaseBySupplier();
if (this.dim === "product")
return this.loadPurchaseByProduct();
}
if (this.mode === "inventory") {
if (this.dim === "qty")
return this.loadInventoryByQty();
if (this.dim === "amount")
return this.loadInventoryByAmount();
}
if (this.mode === "arap") {
if (this.dim === "ar")
return this.loadAR();
if (this.dim === "ap")
return this.loadAP();
}
},
async loadByCustomer() {
try {
const listResp = await common_http.get("/api/orders", { biz: "sale", type: "out", startDate: this.startDate, endDate: this.endDate, page: 1, size: 200 });
const list = listResp && (listResp.list || listResp) || [];
const map = /* @__PURE__ */ new Map();
let totalSales = 0;
for (const it of list) {
const name = it.customerName || "未知客户";
const amount = Number(it.amount || 0);
totalSales += amount;
if (!map.has(name))
map.set(name, { name, sales: 0, cost: 0, profit: 0 });
const row = map.get(name);
row.sales += amount;
}
const rows = Array.from(map.values()).map((r) => ({ ...r, profit: r.sales - r.cost }));
const total = { sales: totalSales, cost: 0, profit: totalSales };
this.rows = rows;
this.total = total;
} catch (e) {
common_vendor.index.showToast({ title: "加载失败", icon: "none" });
}
},
async loadByProduct() {
try {
const listResp = await common_http.get("/api/orders", { biz: "sale", type: "out", startDate: this.startDate, endDate: this.endDate, page: 1, size: 200 });
const list = listResp && (listResp.list || listResp) || [];
const agg = /* @__PURE__ */ new Map();
for (const it of list) {
try {
const d = await common_http.get(`/api/orders/${it.id}`);
const items = d && d.items || [];
for (const m of items) {
const key = String(m.productId || m.name);
if (!agg.has(key))
agg.set(key, { name: m.name || "#" + key, sales: 0, cost: 0, profit: 0 });
const row = agg.get(key);
const sales = Number(m.amount || 0);
row.sales += sales;
}
} catch (_) {
}
}
const rows = Array.from(agg.values()).map((r) => ({ ...r, profit: r.sales - r.cost }));
const totalSales = rows.reduce((s, r) => s + r.sales, 0);
this.rows = rows;
this.total = { sales: totalSales, cost: 0, profit: totalSales };
} catch (e) {
common_vendor.index.showToast({ title: "加载失败", icon: "none" });
}
},
async loadPurchaseBySupplier() {
try {
const listResp = await common_http.get("/api/purchase-orders", { startDate: this.startDate, endDate: this.endDate, page: 1, size: 200 });
const list = listResp && (listResp.list || listResp) || [];
const map = /* @__PURE__ */ new Map();
let total = 0;
for (const it of list) {
const name = it.supplierName || "未知供应商";
const amount = Number(it.amount || 0);
total += amount;
if (!map.has(name))
map.set(name, { name, sales: 0, cost: 0, profit: 0 });
const row = map.get(name);
row.sales += amount;
}
this.rows = Array.from(map.values());
this.total = { sales: total, cost: 0, profit: 0 };
} catch (e) {
common_vendor.index.showToast({ title: "加载失败", icon: "none" });
}
},
async loadPurchaseByProduct() {
try {
const listResp = await common_http.get("/api/purchase-orders", { startDate: this.startDate, endDate: this.endDate, page: 1, size: 200 });
const list = listResp && (listResp.list || listResp) || [];
const agg = /* @__PURE__ */ new Map();
for (const it of list) {
try {
const d = await common_http.get(`/api/purchase-orders/${it.id}`);
for (const m of (d == null ? void 0 : d.items) || []) {
const key = String(m.productId || m.name);
if (!agg.has(key))
agg.set(key, { name: m.name || "#" + key, sales: 0, cost: 0, profit: 0 });
const row = agg.get(key);
row.sales += Number(m.amount || 0);
}
} catch (_) {
}
}
const rows = Array.from(agg.values());
const total = rows.reduce((s, r) => s + r.sales, 0);
this.rows = rows;
this.total = { sales: total, cost: 0, profit: 0 };
} catch (e) {
common_vendor.index.showToast({ title: "加载失败", icon: "none" });
}
},
async loadInventoryByQty() {
try {
const resp = await common_http.get("/api/inventories/logs", { startDate: this.startDate, endDate: this.endDate, page: 1, size: 200 });
const list = resp && (resp.list || resp) || [];
const map = /* @__PURE__ */ new Map();
let totalQty = 0;
for (const it of list) {
const key = it.productId || "未知";
if (!map.has(key))
map.set(key, { name: String(key), sales: 0, cost: 0, profit: 0 });
const row = map.get(key);
const q = Number(it.qtyDelta || 0);
row.sales += q;
totalQty += q;
}
this.rows = Array.from(map.values());
this.total = { sales: totalQty, cost: 0, profit: 0 };
} catch (e) {
common_vendor.index.showToast({ title: "加载失败", icon: "none" });
}
},
async loadInventoryByAmount() {
try {
const resp = await common_http.get("/api/inventories/logs", { startDate: this.startDate, endDate: this.endDate, page: 1, size: 200 });
const list = resp && (resp.list || resp) || [];
const map = /* @__PURE__ */ new Map();
let totalAmt = 0;
for (const it of list) {
const key = it.productId || "未知";
if (!map.has(key))
map.set(key, { name: String(key), sales: 0, cost: 0, profit: 0 });
const row = map.get(key);
const a = Number(it.amount || it.amountDelta || 0);
row.sales += a;
totalAmt += a;
}
this.rows = Array.from(map.values());
this.total = { sales: totalAmt, cost: 0, profit: 0 };
} catch (e) {
common_vendor.index.showToast({ title: "加载失败", icon: "none" });
}
},
async loadAR() {
try {
const res = await common_http.get("/api/customers", { page: 1, size: 100, debtOnly: false });
const list = Array.isArray(res == null ? void 0 : res.list) ? res.list : Array.isArray(res) ? res : [];
const rows = list.map((c) => ({ name: c.name, sales: Number(c.receivable || 0), cost: 0, profit: 0 }));
const total = rows.reduce((s, r) => s + r.sales, 0);
this.rows = rows;
this.total = { sales: total, cost: 0, profit: 0 };
} catch (e) {
common_vendor.index.showToast({ title: "加载失败", icon: "none" });
}
},
async loadAP() {
try {
const res = await common_http.get("/api/suppliers", { page: 1, size: 100 });
const list = Array.isArray(res == null ? void 0 : res.list) ? res.list : Array.isArray(res) ? res : [];
const rows = list.map((s) => ({ name: s.name, sales: Number(s.apPayable || 0), cost: 0, profit: 0 }));
const total = rows.reduce((s, r) => s + r.sales, 0);
this.rows = rows;
this.total = { sales: total, cost: 0, profit: 0 };
} catch (e) {
common_vendor.index.showToast({ title: "加载失败", icon: "none" });
}
}
}
};
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
return common_vendor.e({
a: $data.mode === "sale" ? 1 : "",
b: common_vendor.o(($event) => $options.setMode("sale")),
c: $data.mode === "purchase" ? 1 : "",
d: common_vendor.o(($event) => $options.setMode("purchase")),
e: $data.mode === "inventory" ? 1 : "",
f: common_vendor.o(($event) => $options.setMode("inventory")),
g: $data.mode === "arap" ? 1 : "",
h: common_vendor.o(($event) => $options.setMode("arap")),
i: common_vendor.t($data.startDate),
j: $data.startDate,
k: common_vendor.o((...args) => $options.onStartChange && $options.onStartChange(...args)),
l: common_vendor.t($data.endDate),
m: $data.endDate,
n: common_vendor.o((...args) => $options.onEndChange && $options.onEndChange(...args)),
o: $data.mode === "sale"
}, $data.mode === "sale" ? {
p: $data.dim === "customer" ? 1 : "",
q: common_vendor.o(($event) => {
$data.dim = "customer";
$options.refresh();
}),
r: $data.dim === "product" ? 1 : "",
s: common_vendor.o(($event) => {
$data.dim = "product";
$options.refresh();
})
} : $data.mode === "purchase" ? {
v: $data.dim === "supplier" ? 1 : "",
w: common_vendor.o(($event) => {
$data.dim = "supplier";
$options.refresh();
}),
x: $data.dim === "product" ? 1 : "",
y: common_vendor.o(($event) => {
$data.dim = "product";
$options.refresh();
})
} : $data.mode === "inventory" ? {
A: $data.dim === "qty" ? 1 : "",
B: common_vendor.o(($event) => {
$data.dim = "qty";
$options.refresh();
}),
C: $data.dim === "amount" ? 1 : "",
D: common_vendor.o(($event) => {
$data.dim = "amount";
$options.refresh();
})
} : $data.mode === "arap" ? {
F: $data.dim === "ar" ? 1 : "",
G: common_vendor.o(($event) => {
$data.dim = "ar";
$options.refresh();
}),
H: $data.dim === "ap" ? 1 : "",
I: common_vendor.o(($event) => {
$data.dim = "ap";
$options.refresh();
})
} : {}, {
t: $data.mode === "purchase",
z: $data.mode === "inventory",
E: $data.mode === "arap",
J: common_vendor.t($options.fmt($data.total.sales)),
K: common_vendor.t($options.fmt($data.total.cost)),
L: common_vendor.t($options.fmt($data.total.profit)),
M: common_vendor.t($options.profitRate),
N: common_vendor.f($data.rows, (row, idx, i0) => {
return common_vendor.e({
a: row.avatar
}, row.avatar ? {
b: row.avatar
} : {}, {
c: common_vendor.t(row.name),
d: common_vendor.t($options.fmt(row.sales)),
e: common_vendor.t($options.fmt(row.cost)),
f: common_vendor.t($options.fmt(row.profit)),
g: idx
});
})
});
}
const MiniProgramPage = /* @__PURE__ */ common_vendor._export_sfc(_sfc_main, [["render", _sfc_render]]);
wx.createPage(MiniProgramPage);
//# sourceMappingURL=../../../.sourcemap/mp-weixin/pages/report/index.js.map

View File

@@ -0,0 +1,4 @@
{
"navigationBarTitleText": "报表",
"usingComponents": {}
}

View File

@@ -0,0 +1 @@
<view class="report"><view class="modes"><view class="{{['mode-tab', a && 'active']}}" bindtap="{{b}}">销售统计</view><view class="{{['mode-tab', c && 'active']}}" bindtap="{{d}}">进货统计</view><view class="{{['mode-tab', e && 'active']}}" bindtap="{{f}}">库存统计</view><view class="{{['mode-tab', g && 'active']}}" bindtap="{{h}}">应收/应付对账</view></view><view class="toolbar"><picker mode="date" value="{{j}}" bindchange="{{k}}"><view class="date">{{i}}</view></picker><text style="margin:0 8rpx">—</text><picker mode="date" value="{{m}}" bindchange="{{n}}"><view class="date">{{l}}</view></picker></view><view wx:if="{{o}}" class="tabs"><view class="{{['tab', p && 'active']}}" bindtap="{{q}}">按客户</view><view class="{{['tab', r && 'active']}}" bindtap="{{s}}">按货品</view></view><view wx:elif="{{t}}" class="tabs"><view class="{{['tab', v && 'active']}}" bindtap="{{w}}">按供应商</view><view class="{{['tab', x && 'active']}}" bindtap="{{y}}">按货品</view></view><view wx:elif="{{z}}" class="tabs"><view class="{{['tab', A && 'active']}}" bindtap="{{B}}">按数量</view><view class="{{['tab', C && 'active']}}" bindtap="{{D}}">按金额</view></view><view wx:elif="{{E}}" class="tabs"><view class="{{['tab', F && 'active']}}" bindtap="{{G}}">应收对账</view><view class="{{['tab', H && 'active']}}" bindtap="{{I}}">应付对账</view></view><view class="summary"><view class="item"><text class="label">销售额</text><text class="value">¥ {{J}}</text></view><view class="item"><text class="label">成本</text><text class="value">¥ {{K}}</text></view><view class="item"><text class="label">利润</text><text class="value">¥ {{L}}</text></view><view class="item"><text class="label">利润率</text><text class="value">{{M}}</text></view></view><view wx:for="{{N}}" wx:for-item="row" wx:key="g" class="card"><view class="row-head"><image wx:if="{{row.a}}" class="thumb" src="{{row.b}}"/><view class="title">{{row.c}}</view></view><view class="row-body"><text>销售额:¥ {{row.d}}</text><text style="margin-left:18rpx">成本:¥ {{row.e}}</text><text style="margin-left:18rpx">利润:¥ {{row.f}}</text></view></view></view>

View File

@@ -0,0 +1,37 @@
.report { padding: 20rpx;
}
.modes { display: flex; gap: 12rpx; margin-bottom: 14rpx;
}
.mode-tab { flex: 1; text-align: center; padding: 16rpx 0; border-radius: 999rpx; background: #f4f4f4; color: #666; border: 1rpx solid #e9e9e9;
}
.mode-tab.active { background: #1aad19; color: #fff; border-color: #1aad19; font-weight: 700;
}
.toolbar { display: flex; align-items: center; gap: 8rpx; background: #fff; padding: 14rpx 16rpx; border-radius: 12rpx;
}
.date { padding: 10rpx 16rpx; border: 1rpx solid #eee; border-radius: 8rpx;
}
.tabs { display: flex; gap: 16rpx; margin-top: 14rpx;
}
.tab { padding: 12rpx 18rpx; border-radius: 999rpx; background: #f4f4f4; color: #666;
}
.tab.active { background: #1aad19; color: #fff;
}
.summary { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8rpx; margin-top: 14rpx;
}
.summary .item { background: #fff; border-radius: 12rpx; padding: 16rpx;
}
.summary .label { font-size: 22rpx; color: #888;
}
.summary .value { display: block; margin-top: 8rpx; font-weight: 700; color: #333;
}
.card { margin-top: 16rpx; background: #fff; border-radius: 12rpx; padding: 16rpx;
}
.row-head { display: flex; align-items: center; gap: 12rpx;
}
.thumb { width: 72rpx; height: 72rpx; border-radius: 8rpx; background: #f2f2f2;
}
.title { font-size: 28rpx; font-weight: 700;
}
.row-body { margin-top: 10rpx; color: #666;
}

View File

@@ -12,25 +12,31 @@
前端1.货品功能√
后端1.首页核心数据√
2.所有图片用占位图backend\picture代替
3.货品功能√(还有问题)
9.18王德鹏1
前端1.明细功能√
后端1.数据库驱动改为硬编码
2.货品功能√
3.开单(未全完成)
9.18王德鹏2
前端1.客户功能√
后端1.客户功能√(未全完成)
数据库1.客户表更改
9.18王德鹏3
前端1.供应商功能√
后端1.客户功能√
2.供应商功能√
2.供应商功能√v
3.开单功能(销售进货)√
9.20王德鹏1
前端1.账户功能√
2.我的功能√
3.报表功能√
后端1.账户功能√
2.开单功能√
3.明细功能√
4.报表功能√