9.20/1
This commit is contained in:
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 停用
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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; // 可空
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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:支付宝}
|
||||
|
||||
@@ -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`
|
||||
|
||||
257
doc/openapi.yaml
257
doc/openapi.yaml
@@ -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|purchase,type=out|in|return;返回 {list:[]}。
|
||||
summary: 单据列表查询(✅ Fully Implemented)
|
||||
description: 支持按时间范围与关键字筛选;参数 biz=sale|purchase,type=out|in|return;返回 {list:[]}。后端已返回驼峰字段与名称(sale: customerName;purchase: 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
|
||||
|
||||
@@ -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": {
|
||||
|
||||
74
frontend/pages/account/form.vue
Normal file
74
frontend/pages/account/form.vue
Normal 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>
|
||||
|
||||
|
||||
87
frontend/pages/account/ledger.vue
Normal file
87
frontend/pages/account/ledger.vue
Normal 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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
|
||||
@@ -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: '广告',
|
||||
|
||||
59
frontend/pages/my/about.vue
Normal file
59
frontend/pages/my/about.vue
Normal 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
152
frontend/pages/my/index.vue
Normal 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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
|
||||
44
frontend/pages/report/entry.vue
Normal file
44
frontend/pages/report/entry.vue
Normal 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>
|
||||
|
||||
|
||||
291
frontend/pages/report/index.vue
Normal file
291
frontend/pages/report/index.vue
Normal 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>
|
||||
|
||||
|
||||
@@ -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
1
frontend/unpackage/dist/dev/.sourcemap/mp-weixin/pages/account/form.js.map
vendored
Normal file
1
frontend/unpackage/dist/dev/.sourcemap/mp-weixin/pages/account/form.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
1
frontend/unpackage/dist/dev/.sourcemap/mp-weixin/pages/account/ledger.js.map
vendored
Normal file
1
frontend/unpackage/dist/dev/.sourcemap/mp-weixin/pages/account/ledger.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
||||
{"version":3,"file":"select.js","sources":["pages/account/select.vue","../../../../Downloads/HBuilderX.4.76.2025082103/HBuilderX/plugins/uniapp-cli-vite/uniPage:/cGFnZXMvYWNjb3VudC9zZWxlY3QudnVl"],"sourcesContent":["<template>\r\n\t<view class=\"page\">\r\n\t\t<scroll-view scroll-y class=\"list\">\r\n\t\t\t<view class=\"item\" v-for=\"a in accounts\" :key=\"a.id\" @click=\"select(a)\">\r\n\t\t\t\t<view class=\"name\">{{ a.name }}</view>\r\n\t\t\t\t<view class=\"meta\">{{ typeLabel(a.type) }} · 余额:{{ a.balance?.toFixed ? a.balance.toFixed(2) : a.balance }}</view>\r\n\t\t\t</view>\r\n\t\t</scroll-view>\r\n\t</view>\r\n</template>\r\n\r\n<script>\r\n\timport { get } from '../../common/http.js'\r\n\tconst TYPE_MAP = { cash: '现金', bank: '银行', alipay: '支付宝', wechat: '微信', other: '其他' }\r\n\texport default {\r\n\t\tdata() { return { accounts: [] } },\r\n\t\tasync onLoad() {\r\n\t\t\ttry {\r\n\t\t\t\tconst res = await get('/api/accounts')\r\n\t\t\t\tthis.accounts = Array.isArray(res) ? res : (res?.list || [])\r\n\t\t\t} catch(e) { uni.showToast({ title: '加载失败', icon: 'none' }) }\r\n\t\t},\r\n\t\tmethods: {\r\n\t\t\tselect(a) {\r\n\t\t\t\tconst opener = getCurrentPages()[getCurrentPages().length-2]\r\n\t\t\t\tif (opener && opener.$vm) {\r\n\t\t\t\t\topener.$vm.selectedAccountId = a.id\r\n\t\t\t\t\topener.$vm.selectedAccountName = a.name\r\n\t\t\t\t}\r\n\t\t\t\tuni.navigateBack()\r\n\t\t\t},\r\n\t\t\ttypeLabel(t) { return TYPE_MAP[t] || t }\r\n\t\t}\r\n\t}\r\n</script>\r\n\r\n<style>\r\n\t.page { display:flex; flex-direction: column; height: 100vh; }\r\n\t.list { flex:1; }\r\n\t.item { padding: 20rpx 24rpx; background:#fff; border-bottom: 1rpx solid #f1f1f1; }\r\n\t.name { color:#333; margin-bottom: 6rpx; }\r\n\t.meta { color:#888; font-size: 24rpx; }\r\n</style>\r\n\r\n\r\n\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
1
frontend/unpackage/dist/dev/.sourcemap/mp-weixin/pages/my/about.js.map
vendored
Normal file
1
frontend/unpackage/dist/dev/.sourcemap/mp-weixin/pages/my/about.js.map
vendored
Normal 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;"}
|
||||
1
frontend/unpackage/dist/dev/.sourcemap/mp-weixin/pages/my/index.js.map
vendored
Normal file
1
frontend/unpackage/dist/dev/.sourcemap/mp-weixin/pages/my/index.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
frontend/unpackage/dist/dev/.sourcemap/mp-weixin/pages/report/entry.js.map
vendored
Normal file
1
frontend/unpackage/dist/dev/.sourcemap/mp-weixin/pages/report/entry.js.map
vendored
Normal 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;"}
|
||||
1
frontend/unpackage/dist/dev/.sourcemap/mp-weixin/pages/report/index.js.map
vendored
Normal file
1
frontend/unpackage/dist/dev/.sourcemap/mp-weixin/pages/report/index.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
6
frontend/unpackage/dist/dev/mp-weixin/app.js
vendored
6
frontend/unpackage/dist/dev/mp-weixin/app.js
vendored
@@ -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() {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
102
frontend/unpackage/dist/dev/mp-weixin/pages/account/form.js
vendored
Normal file
102
frontend/unpackage/dist/dev/mp-weixin/pages/account/form.js
vendored
Normal 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
|
||||
4
frontend/unpackage/dist/dev/mp-weixin/pages/account/form.json
vendored
Normal file
4
frontend/unpackage/dist/dev/mp-weixin/pages/account/form.json
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"navigationBarTitleText": "新增/编辑账户",
|
||||
"usingComponents": {}
|
||||
}
|
||||
1
frontend/unpackage/dist/dev/mp-weixin/pages/account/form.wxml
vendored
Normal file
1
frontend/unpackage/dist/dev/mp-weixin/pages/account/form.wxml
vendored
Normal 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>
|
||||
23
frontend/unpackage/dist/dev/mp-weixin/pages/account/form.wxss
vendored
Normal file
23
frontend/unpackage/dist/dev/mp-weixin/pages/account/form.wxss
vendored
Normal 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;
|
||||
}
|
||||
83
frontend/unpackage/dist/dev/mp-weixin/pages/account/ledger.js
vendored
Normal file
83
frontend/unpackage/dist/dev/mp-weixin/pages/account/ledger.js
vendored
Normal 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
|
||||
4
frontend/unpackage/dist/dev/mp-weixin/pages/account/ledger.json
vendored
Normal file
4
frontend/unpackage/dist/dev/mp-weixin/pages/account/ledger.json
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"navigationBarTitleText": "账户流水",
|
||||
"usingComponents": {}
|
||||
}
|
||||
1
frontend/unpackage/dist/dev/mp-weixin/pages/account/ledger.wxml
vendored
Normal file
1
frontend/unpackage/dist/dev/mp-weixin/pages/account/ledger.wxml
vendored
Normal 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>
|
||||
35
frontend/unpackage/dist/dev/mp-weixin/pages/account/ledger.wxss
vendored
Normal file
35
frontend/unpackage/dist/dev/mp-weixin/pages/account/ledger.wxss
vendored
Normal 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;
|
||||
}
|
||||
@@ -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]]);
|
||||
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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]]);
|
||||
|
||||
27
frontend/unpackage/dist/dev/mp-weixin/pages/my/about.js
vendored
Normal file
27
frontend/unpackage/dist/dev/mp-weixin/pages/my/about.js
vendored
Normal 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
|
||||
4
frontend/unpackage/dist/dev/mp-weixin/pages/my/about.json
vendored
Normal file
4
frontend/unpackage/dist/dev/mp-weixin/pages/my/about.json
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"navigationBarTitleText": "关于与协议",
|
||||
"usingComponents": {}
|
||||
}
|
||||
1
frontend/unpackage/dist/dev/mp-weixin/pages/my/about.wxml
vendored
Normal file
1
frontend/unpackage/dist/dev/mp-weixin/pages/my/about.wxml
vendored
Normal 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>
|
||||
21
frontend/unpackage/dist/dev/mp-weixin/pages/my/about.wxss
vendored
Normal file
21
frontend/unpackage/dist/dev/mp-weixin/pages/my/about.wxss
vendored
Normal 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;
|
||||
}
|
||||
110
frontend/unpackage/dist/dev/mp-weixin/pages/my/index.js
vendored
Normal file
110
frontend/unpackage/dist/dev/mp-weixin/pages/my/index.js
vendored
Normal 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
|
||||
4
frontend/unpackage/dist/dev/mp-weixin/pages/my/index.json
vendored
Normal file
4
frontend/unpackage/dist/dev/mp-weixin/pages/my/index.json
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"navigationBarTitleText": "我的",
|
||||
"usingComponents": {}
|
||||
}
|
||||
1
frontend/unpackage/dist/dev/mp-weixin/pages/my/index.wxml
vendored
Normal file
1
frontend/unpackage/dist/dev/mp-weixin/pages/my/index.wxml
vendored
Normal 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>
|
||||
27
frontend/unpackage/dist/dev/mp-weixin/pages/my/index.wxss
vendored
Normal file
27
frontend/unpackage/dist/dev/mp-weixin/pages/my/index.wxss
vendored
Normal 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;
|
||||
}
|
||||
@@ -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]]);
|
||||
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
25
frontend/unpackage/dist/dev/mp-weixin/pages/report/entry.js
vendored
Normal file
25
frontend/unpackage/dist/dev/mp-weixin/pages/report/entry.js
vendored
Normal 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
|
||||
4
frontend/unpackage/dist/dev/mp-weixin/pages/report/entry.json
vendored
Normal file
4
frontend/unpackage/dist/dev/mp-weixin/pages/report/entry.json
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"navigationBarTitleText": "报表",
|
||||
"usingComponents": {}
|
||||
}
|
||||
1
frontend/unpackage/dist/dev/mp-weixin/pages/report/entry.wxml
vendored
Normal file
1
frontend/unpackage/dist/dev/mp-weixin/pages/report/entry.wxml
vendored
Normal 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>
|
||||
13
frontend/unpackage/dist/dev/mp-weixin/pages/report/entry.wxss
vendored
Normal file
13
frontend/unpackage/dist/dev/mp-weixin/pages/report/entry.wxss
vendored
Normal 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;
|
||||
}
|
||||
338
frontend/unpackage/dist/dev/mp-weixin/pages/report/index.js
vendored
Normal file
338
frontend/unpackage/dist/dev/mp-weixin/pages/report/index.js
vendored
Normal 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
|
||||
4
frontend/unpackage/dist/dev/mp-weixin/pages/report/index.json
vendored
Normal file
4
frontend/unpackage/dist/dev/mp-weixin/pages/report/index.json
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"navigationBarTitleText": "报表",
|
||||
"usingComponents": {}
|
||||
}
|
||||
1
frontend/unpackage/dist/dev/mp-weixin/pages/report/index.wxml
vendored
Normal file
1
frontend/unpackage/dist/dev/mp-weixin/pages/report/index.wxml
vendored
Normal 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>
|
||||
37
frontend/unpackage/dist/dev/mp-weixin/pages/report/index.wxss
vendored
Normal file
37
frontend/unpackage/dist/dev/mp-weixin/pages/report/index.wxss
vendored
Normal 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;
|
||||
}
|
||||
14
沟通.md
14
沟通.md
@@ -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.报表功能√
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user