细节修改
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
18631081161 2026-04-13 14:25:51 +08:00
parent d43406380c
commit 539b58ea87
21 changed files with 1052 additions and 84 deletions

View File

@ -52,4 +52,7 @@ declare module 'vue' {
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
}
export interface GlobalDirectives {
vLoading: typeof import('element-plus/es')['ElLoadingDirective']
}
}

View File

@ -10,3 +10,4 @@ export * from './points'
export * from './stamp'
export * from './upload'
export * from './user'
export * from './testAccount'

View File

@ -0,0 +1,18 @@
import request from '@/utils/request'
// 测试账号 API
/** 获取测试账号列表 */
export function getTestAccounts() {
return request.get('/test-account')
}
/** 新增/更新测试账号 */
export function saveTestAccount(data: { phone: string; code: string }) {
return request.post('/test-account', data)
}
/** 删除测试账号 */
export function deleteTestAccount(phone: string) {
return request.delete(`/test-account/${encodeURIComponent(phone)}`)
}

View File

@ -96,6 +96,7 @@ const menuItems = [
{ path: '/points', title: '积分配置', icon: 'Coin' },
{ path: '/user', title: '用户管理', icon: 'User' },
{ path: '/content', title: '内容管理', icon: 'Document' },
{ path: '/test-account', title: '测试账号', icon: 'Iphone' },
]
function handleCommand(command: string) {

View File

@ -69,6 +69,12 @@ const routes: RouteRecordRaw[] = [
component: () => import('@/views/content/index.vue'),
meta: { title: '内容管理', icon: 'Document' },
},
{
path: 'test-account',
name: 'TestAccount',
component: () => import('@/views/test-account/index.vue'),
meta: { title: '测试账号', icon: 'Iphone' },
},
],
},
]

View File

@ -0,0 +1,123 @@
<template>
<div class="test-account">
<!-- 添加表单 -->
<el-card shadow="never" style="margin-bottom: 20px">
<template #header>
<span>添加测试账号</span>
</template>
<el-form :model="form" :rules="rules" ref="formRef" inline>
<el-form-item label="手机号" prop="phone">
<el-input v-model="form.phone" placeholder="请输入手机号" style="width: 200px" />
</el-form-item>
<el-form-item label="验证码" prop="code">
<el-input v-model="form.code" placeholder="请输入固定验证码" style="width: 160px" />
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="saving" @click="handleSave">保存</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- 列表 -->
<el-table :data="list" border style="width: 100%" v-loading="loading">
<el-table-column label="手机号" prop="phone" width="200" />
<el-table-column label="验证码" prop="code" width="160" />
<el-table-column label="创建时间" min-width="180">
<template #default="{ row }">{{ formatDate(row.createdAt) }}</template>
</el-table-column>
<el-table-column label="操作" width="120" align="center">
<template #default="{ row }">
<el-button size="small" type="danger" @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import type { FormInstance, FormRules } from 'element-plus'
import { ElMessage, ElMessageBox } from 'element-plus'
import { getTestAccounts, saveTestAccount, deleteTestAccount } from '@/api/testAccount'
interface TestAccount {
phone: string
code: string
createdAt?: string
}
const formRef = ref<FormInstance>()
const form = reactive({ phone: '', code: '' })
const rules: FormRules = {
phone: [{ required: true, message: '请输入手机号', trigger: 'blur' }],
code: [{ required: true, message: '请输入验证码', trigger: 'blur' }],
}
const list = ref<TestAccount[]>([])
const loading = ref(false)
const saving = ref(false)
//
function formatDate(dateStr: string) {
if (!dateStr) return '-'
return new Date(dateStr).toLocaleString('zh-CN')
}
//
async function loadList() {
loading.value = true
try {
const res: any = await getTestAccounts()
list.value = res.data || []
} catch {
//
} finally {
loading.value = false
}
}
//
async function handleSave() {
if (!formRef.value) return
await formRef.value.validate()
saving.value = true
try {
await saveTestAccount({ phone: form.phone, code: form.code })
ElMessage.success('保存成功')
form.phone = ''
form.code = ''
formRef.value.resetFields()
await loadList()
} catch {
//
} finally {
saving.value = false
}
}
//
async function handleDelete(row: TestAccount) {
try {
await ElMessageBox.confirm(`确定删除测试账号 ${row.phone}`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
await deleteTestAccount(row.phone)
ElMessage.success('已删除')
await loadList()
} catch {
//
}
}
onMounted(() => {
loadList()
})
</script>
<style scoped>
.test-account {
padding: 20px;
}
</style>

View File

@ -0,0 +1,88 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using VendingMachine.Infrastructure.Data;
using VendingMachine.Domain.Entities;
namespace VendingMachine.Api.Controllers;
/// <summary>
/// 管理后台 - 测试账号管理
/// </summary>
[ApiController]
[Route("api/admin/test-account")]
[Authorize]
public class AdminTestAccountController : ControllerBase
{
private readonly AppDbContext _db;
public AdminTestAccountController(AppDbContext db)
{
_db = db;
}
/// <summary>
/// 获取测试账号列表
/// </summary>
[HttpGet]
public async Task<IActionResult> GetAll()
{
var list = await _db.TestAccounts
.OrderByDescending(t => t.CreatedAt)
.ToListAsync();
return Ok(new { success = true, data = list });
}
/// <summary>
/// 新增或更新测试账号
/// </summary>
[HttpPost]
public async Task<IActionResult> Save([FromBody] SaveTestAccountRequest req)
{
if (string.IsNullOrWhiteSpace(req.Phone) || string.IsNullOrWhiteSpace(req.Code))
return Ok(new { success = false, message = "手机号和验证码不能为空" });
var existing = await _db.TestAccounts
.FirstOrDefaultAsync(t => t.Phone == req.Phone);
if (existing != null)
{
// 已存在则更新验证码
existing.Code = req.Code;
}
else
{
_db.TestAccounts.Add(new TestAccount
{
Phone = req.Phone,
Code = req.Code
});
}
await _db.SaveChangesAsync();
return Ok(new { success = true, message = "保存成功" });
}
/// <summary>
/// 删除测试账号
/// </summary>
[HttpDelete("{phone}")]
public async Task<IActionResult> Delete(string phone)
{
var entity = await _db.TestAccounts
.FirstOrDefaultAsync(t => t.Phone == phone);
if (entity == null)
return Ok(new { success = false, message = "测试账号不存在" });
_db.TestAccounts.Remove(entity);
await _db.SaveChangesAsync();
return Ok(new { success = true, message = "已删除" });
}
public class SaveTestAccountRequest
{
public string Phone { get; set; } = string.Empty;
public string Code { get; set; } = string.Empty;
}
}

View File

@ -0,0 +1,12 @@
namespace VendingMachine.Domain.Entities;
/// <summary>
/// 测试账号实体
/// </summary>
public class TestAccount
{
public int Id { get; set; }
public string Phone { get; set; } = string.Empty;
public string Code { get; set; } = string.Empty;
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
}

View File

@ -18,6 +18,7 @@ public class AppDbContext : DbContext
public DbSet<ContentConfig> ContentConfigs => Set<ContentConfig>();
public DbSet<VendingPaymentRecord> VendingPaymentRecords => Set<VendingPaymentRecord>();
public DbSet<AdminUser> AdminUsers => Set<AdminUser>();
public DbSet<TestAccount> TestAccounts => Set<TestAccount>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
@ -115,5 +116,13 @@ public class AppDbContext : DbContext
e.Property(a => a.PasswordHash).HasMaxLength(200);
e.HasIndex(a => a.Username).IsUnique();
});
modelBuilder.Entity<TestAccount>(e =>
{
e.HasKey(t => t.Id);
e.Property(t => t.Phone).HasMaxLength(20);
e.Property(t => t.Code).HasMaxLength(20);
e.HasIndex(t => t.Phone).IsUnique();
});
}
}

View File

@ -8,7 +8,7 @@ public class DesignTimeDbContextFactory : IDesignTimeDbContextFactory<AppDbConte
public AppDbContext CreateDbContext(string[] args)
{
var optionsBuilder = new DbContextOptionsBuilder<AppDbContext>();
optionsBuilder.UseSqlServer("Server=localhost;Database=VendingMachineDb;Trusted_Connection=True;TrustServerCertificate=True;");
optionsBuilder.UseSqlServer("Server=tcp:192.168.195.15,1433;Database=VendingMachineDb;User Id=sa;Password=Dbt@com@123;TrustServerCertificate=True;Encrypt=False;");
return new AppDbContext(optionsBuilder.Options);
}
}

View File

@ -0,0 +1,461 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using VendingMachine.Infrastructure.Data;
#nullable disable
namespace VendingMachine.Infrastructure.Data.Migrations
{
[DbContext(typeof(AppDbContext))]
[Migration("20260413055438_AddTestAccount")]
partial class AddTestAccount
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "10.0.5")
.HasAnnotation("Relational:MaxIdentifierLength", 128);
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
modelBuilder.Entity("VendingMachine.Domain.Entities.AdminUser", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<string>("PasswordHash")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.Property<string>("Username")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.HasKey("Id");
b.HasIndex("Username")
.IsUnique();
b.ToTable("AdminUsers");
});
modelBuilder.Entity("VendingMachine.Domain.Entities.Banner", b =>
{
b.Property<string>("Id")
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<string>("ImageUrlEn")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("ImageUrlZhCn")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("ImageUrlZhTw")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("LinkType")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("nvarchar(20)");
b.Property<string>("LinkUrl")
.HasColumnType("nvarchar(max)");
b.Property<int>("SortOrder")
.HasColumnType("int");
b.HasKey("Id");
b.ToTable("Banners");
});
modelBuilder.Entity("VendingMachine.Domain.Entities.ContentConfig", b =>
{
b.Property<string>("Key")
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<string>("ContentEn")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("ContentZhCn")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("ContentZhTw")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("datetime2");
b.HasKey("Key");
b.ToTable("ContentConfigs");
});
modelBuilder.Entity("VendingMachine.Domain.Entities.CouponTemplate", b =>
{
b.Property<string>("Id")
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<decimal>("DiscountAmount")
.HasPrecision(18, 2)
.HasColumnType("decimal(18,2)");
b.Property<DateTime>("ExpireAt")
.HasColumnType("datetime2");
b.Property<bool>("IsActive")
.HasColumnType("bit");
b.Property<bool>("IsStamp")
.HasColumnType("bit");
b.Property<string>("NameEn")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("NameZhCn")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("NameZhTw")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<int>("PointsCost")
.HasColumnType("int");
b.Property<decimal?>("ThresholdAmount")
.HasPrecision(18, 2)
.HasColumnType("decimal(18,2)");
b.Property<int>("Type")
.HasColumnType("int");
b.HasKey("Id");
b.ToTable("CouponTemplates");
});
modelBuilder.Entity("VendingMachine.Domain.Entities.HomeEntry", b =>
{
b.Property<string>("Id")
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<string>("ImageUrlEn")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("ImageUrlZhCn")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("ImageUrlZhTw")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("Type")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("nvarchar(20)");
b.HasKey("Id");
b.ToTable("HomeEntries");
});
modelBuilder.Entity("VendingMachine.Domain.Entities.MembershipProduct", b =>
{
b.Property<string>("Id")
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<string>("AppleProductId")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("Currency")
.IsRequired()
.HasMaxLength(10)
.HasColumnType("nvarchar(10)");
b.Property<string>("DescriptionEn")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("DescriptionZhCn")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("DescriptionZhTw")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<int>("DurationDays")
.HasColumnType("int");
b.Property<string>("GoogleProductId")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<decimal>("Price")
.HasPrecision(18, 2)
.HasColumnType("decimal(18,2)");
b.Property<int>("Type")
.HasColumnType("int");
b.HasKey("Id");
b.ToTable("MembershipProducts");
});
modelBuilder.Entity("VendingMachine.Domain.Entities.PointRecord", b =>
{
b.Property<string>("Id")
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<int>("Amount")
.HasColumnType("int");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<string>("Source")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.Property<int>("Type")
.HasColumnType("int");
b.Property<string>("UserId")
.IsRequired()
.HasMaxLength(12)
.HasColumnType("nvarchar(12)");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("PointRecords");
});
modelBuilder.Entity("VendingMachine.Domain.Entities.PointsConfig", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<decimal>("ConversionRate")
.HasPrecision(18, 4)
.HasColumnType("decimal(18,4)");
b.HasKey("Id");
b.ToTable("PointsConfigs");
});
modelBuilder.Entity("VendingMachine.Domain.Entities.TestAccount", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("Code")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("nvarchar(20)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<string>("Phone")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("nvarchar(20)");
b.HasKey("Id");
b.HasIndex("Phone")
.IsUnique();
b.ToTable("TestAccounts");
});
modelBuilder.Entity("VendingMachine.Domain.Entities.User", b =>
{
b.Property<string>("Uid")
.HasMaxLength(12)
.HasColumnType("nvarchar(12)");
b.Property<string>("AreaCode")
.IsRequired()
.HasMaxLength(10)
.HasColumnType("nvarchar(10)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<bool>("IsDeleted")
.HasColumnType("bit");
b.Property<bool>("IsMember")
.HasColumnType("bit");
b.Property<string>("Language")
.IsRequired()
.HasMaxLength(10)
.HasColumnType("nvarchar(10)");
b.Property<DateTime?>("MembershipExpireAt")
.HasColumnType("datetime2");
b.Property<int>("MembershipType")
.HasColumnType("int");
b.Property<string>("Nickname")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<string>("Phone")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("nvarchar(20)");
b.Property<int>("PointsBalance")
.HasColumnType("int");
b.Property<DateTime?>("PointsExpireAt")
.HasColumnType("datetime2");
b.HasKey("Uid");
b.ToTable("Users");
});
modelBuilder.Entity("VendingMachine.Domain.Entities.UserCoupon", b =>
{
b.Property<string>("Id")
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<string>("CouponTemplateId")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<DateTime>("ExpireAt")
.HasColumnType("datetime2");
b.Property<int>("Status")
.HasColumnType("int");
b.Property<DateTime?>("UsedAt")
.HasColumnType("datetime2");
b.Property<string>("UserId")
.IsRequired()
.HasMaxLength(12)
.HasColumnType("nvarchar(12)");
b.HasKey("Id");
b.HasIndex("CouponTemplateId");
b.HasIndex("UserId");
b.ToTable("UserCoupons");
});
modelBuilder.Entity("VendingMachine.Domain.Entities.VendingPaymentRecord", b =>
{
b.Property<string>("Id")
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<string>("MachineId")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<decimal>("PaymentAmount")
.HasPrecision(18, 2)
.HasColumnType("decimal(18,2)");
b.Property<string>("PaymentStatus")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("nvarchar(20)");
b.Property<string>("TransactionId")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<string>("UsedCouponId")
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<string>("UserId")
.IsRequired()
.HasMaxLength(12)
.HasColumnType("nvarchar(12)");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("VendingPaymentRecords");
});
modelBuilder.Entity("VendingMachine.Domain.Entities.UserCoupon", b =>
{
b.HasOne("VendingMachine.Domain.Entities.CouponTemplate", "CouponTemplate")
.WithMany()
.HasForeignKey("CouponTemplateId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("CouponTemplate");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,43 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace VendingMachine.Infrastructure.Data.Migrations
{
/// <inheritdoc />
public partial class AddTestAccount : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "TestAccounts",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
Phone = table.Column<string>(type: "nvarchar(20)", maxLength: 20, nullable: false),
Code = table.Column<string>(type: "nvarchar(20)", maxLength: 20, nullable: false),
CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_TestAccounts", x => x.Id);
});
migrationBuilder.CreateIndex(
name: "IX_TestAccounts_Phone",
table: "TestAccounts",
column: "Phone",
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "TestAccounts");
}
}
}

View File

@ -280,6 +280,35 @@ namespace VendingMachine.Infrastructure.Data.Migrations
b.ToTable("PointsConfigs");
});
modelBuilder.Entity("VendingMachine.Domain.Entities.TestAccount", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("Code")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("nvarchar(20)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<string>("Phone")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("nvarchar(20)");
b.HasKey("Id");
b.HasIndex("Phone")
.IsUnique();
b.ToTable("TestAccounts");
});
modelBuilder.Entity("VendingMachine.Domain.Entities.User", b =>
{
b.Property<string>("Uid")

View File

@ -71,7 +71,9 @@ public class UserService : IUserService
return ApiResponse<LoginResponse>.Fail("无效的手机号格式");
// 验证验证码(测试账号跳过验证码校验)
var isTestAccount = request.Phone == "18631081161" && request.Code == "1111";
var testAccount = await _db.TestAccounts
.FirstOrDefaultAsync(t => t.Phone == request.Phone && t.Code == request.Code);
var isTestAccount = testAccount != null;
if (!isTestAccount)
{
var key = $"sms:code:{request.AreaCode}{request.Phone}";

View File

@ -3,6 +3,7 @@ import { getStorage, removeStorage, TOKEN_KEY, LOCALE_KEY } from '../utils/stora
// 后端API基础地址
// MuMu模拟器需使用宿主机局域网IP生产环境替换为正式域名
export const BASE_URL = 'http://192.168.21.7:5082'
// export const BASE_URL = 'http://192.168.21.7:5082'
/**
* 统一请求封装自动注入Token和语言请求头统一处理响应和错误

View File

@ -1,26 +1,26 @@
{
"name": "贩卖机",
"appid": "__UNI__19998C7",
"description": "贩卖机移动端APP",
"versionName": "1.0.0",
"versionCode": "100",
"transformPx": false,
"app-plus": {
"usingComponents": true,
"nvueStyleCompiler": "uni-app",
"compilerVersion": 3,
"splashscreen": {
"alwaysShowBeforeRender": true,
"waiting": true,
"autoclose": true,
"delay": 0
"name" : "贩卖机",
"appid" : "__UNI__19998C7",
"description" : "贩卖机",
"versionName" : "1.0.0",
"versionCode" : "100",
"transformPx" : false,
"app-plus" : {
"usingComponents" : true,
"nvueStyleCompiler" : "uni-app",
"compilerVersion" : 3,
"splashscreen" : {
"alwaysShowBeforeRender" : true,
"waiting" : true,
"autoclose" : true,
"delay" : 0
},
"modules": {
"Payment": {}
"modules" : {
"Payment" : {}
},
"distribute": {
"android": {
"permissions": [
"distribute" : {
"android" : {
"permissions" : [
"<uses-permission android:name=\"android.permission.INTERNET\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
@ -31,35 +31,35 @@
"<uses-permission android:name=\"android.permission.VIBRATE\"/>",
"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>"
],
"google": {
"pay": true
"google" : {
"pay" : true
}
},
"ios": {
"capabilities": {
"entitlements": {
"com.apple.developer.in-app-payments": true
"ios" : {
"capabilities" : {
"entitlements" : {
"com.apple.developer.in-app-payments" : true
}
}
},
"sdkConfigs": {
"payment": {
"google": {},
"appleiap": {}
"sdkConfigs" : {
"payment" : {
"google" : {},
"appleiap" : {}
}
}
}
},
"quickapp": {},
"mp-weixin": {
"appid": "",
"setting": {
"urlCheck": false
"quickapp" : {},
"mp-weixin" : {
"appid" : "",
"setting" : {
"urlCheck" : false
},
"usingComponents": true
"usingComponents" : true
},
"uniStatistics": {
"enable": false
"uniStatistics" : {
"enable" : false
},
"vueVersion": "3"
"vueVersion" : "3"
}

View File

@ -51,18 +51,21 @@
{
"path": "pages/agreement/agreement",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "用户协议"
}
},
{
"path": "pages/privacy/privacy",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "隐私政策"
}
},
{
"path": "pages/about/about",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "关于"
}
}

View File

@ -1,15 +1,28 @@
<template>
<view class="about-page">
<!-- APP信息 -->
<view class="app-info">
<image class="logo" src="/static/logo.png" mode="aspectFit" />
<text class="version">{{ t('about.version', { version: appVersion }) }}</text>
<!-- 自定义导航栏 -->
<view class="custom-nav" :style="{ paddingTop: statusBarHeight + 'px' }">
<view class="nav-inner">
<view class="nav-back" @click="goBack">
<image class="back-icon" src="/static/ic_back2.png" mode="aspectFit" />
</view>
<text class="nav-title">{{ t('about.title') }}</text>
<view class="nav-placeholder" />
</view>
</view>
<!-- 注销账号按钮已登录时显示 -->
<view v-if="userStore.isLoggedIn" class="delete-section">
<view class="delete-btn" @click="showDeleteConfirm = true">
<text class="delete-text">{{ t('about.deleteAccount') }}</text>
<view class="page-body" :style="{ paddingTop: (statusBarHeight + 44) + 'px' }">
<!-- APP信息 -->
<view class="app-info">
<image class="logo" src="/static/logo.png" mode="aspectFit" />
<text class="version">版本 {{ appVersion }}</text>
</view>
<!-- 注销账号按钮已登录时显示 -->
<view v-if="userStore.isLoggedIn" class="delete-section">
<view class="delete-btn" @click="showDeleteConfirm = true">
<text class="delete-text">{{ t('about.deleteAccount') }}</text>
</view>
</view>
</view>
@ -38,8 +51,25 @@ import { useUserStore } from '../../stores/user.js'
const { t } = useI18n()
const userStore = useUserStore()
// manifest
//
const statusBarHeight = ref(0)
try {
const sysInfo = uni.getSystemInfoSync()
statusBarHeight.value = sysInfo.statusBarHeight || 0
} catch (e) {}
function goBack() {
uni.navigateBack()
}
//
const appVersion = ref('1.0.0')
try {
const accountInfo = uni.getAccountInfoSync?.()
if (accountInfo?.miniProgram?.version) {
appVersion.value = accountInfo.miniProgram.version
}
} catch (e) {}
const showDeleteConfirm = ref(false)
@ -65,6 +95,44 @@ async function handleDelete() {
flex-direction: column;
align-items: center;
}
.custom-nav {
position: fixed;
top: 0; left: 0; right: 0;
z-index: 100;
background-color: #DBDBDB;
}
.nav-inner {
height: 44px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 16rpx;
}
.nav-back {
width: 60rpx;
height: 44px;
display: flex;
align-items: center;
justify-content: center;
}
.back-icon {
width: 40rpx;
height: 40rpx;
}
.nav-title {
font-size: 34rpx;
font-weight: 600;
color: #333;
}
.nav-placeholder {
width: 60rpx;
}
.page-body {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
}
.app-info {
display: flex;

View File

@ -1,10 +1,23 @@
<template>
<view class="agreement-page">
<view v-if="loading" class="loading">
<text>{{ t('common.loading') }}</text>
<!-- 自定义导航栏 -->
<view class="custom-nav" :style="{ paddingTop: statusBarHeight + 'px' }">
<view class="nav-inner">
<view class="nav-back" @click="goBack">
<image class="back-icon" src="/static/ic_back2.png" mode="aspectFit" />
</view>
<text class="nav-title">{{ t('agreement.title') }}</text>
<view class="nav-placeholder" />
</view>
</view>
<view v-else class="content">
<rich-text :nodes="content" />
<view class="page-body" :style="{ paddingTop: (statusBarHeight + 44) + 'px' }">
<view v-if="loading" class="loading">
<text>{{ t('common.loading') }}</text>
</view>
<view v-else class="content">
<rich-text :nodes="content" />
</view>
</view>
</view>
</template>
@ -16,45 +29,82 @@ import { getAgreement } from '../../api/content.js'
const { t } = useI18n()
//
const statusBarHeight = ref(0)
try {
const sysInfo = uni.getSystemInfoSync()
statusBarHeight.value = sysInfo.statusBarHeight || 0
} catch (e) {}
const content = ref('')
const loading = ref(true)
function goBack() {
uni.navigateBack()
}
async function loadContent() {
loading.value = true
try {
const res = await getAgreement()
content.value = res.data?.content ?? res.data ?? ''
} catch (e) {
/* 错误已统一处理 */
} finally {
loading.value = false
}
} catch (e) {}
finally { loading.value = false }
}
onMounted(() => {
loadContent()
})
onMounted(() => { loadContent() })
</script>
<style scoped>
.agreement-page {
min-height: 100vh;
background-color: #ffffff;
}
.custom-nav {
position: fixed;
top: 0; left: 0; right: 0;
z-index: 100;
background-color: #DBDBDB;
}
.nav-inner {
height: 44px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 16rpx;
}
.nav-back {
width: 60rpx;
height: 44px;
display: flex;
align-items: center;
justify-content: center;
}
.back-icon {
width: 40rpx;
height: 40rpx;
}
.nav-title {
font-size: 34rpx;
font-weight: 600;
color: #333;
}
.nav-placeholder {
width: 60rpx;
}
.page-body {
padding: 30rpx;
}
.loading {
display: flex;
align-items: center;
justify-content: center;
padding: 60rpx 0;
}
.loading text {
font-size: 28rpx;
color: #999;
}
.content {
font-size: 28rpx;
color: #333;

View File

@ -4,7 +4,7 @@
<view class="custom-nav" :style="{ paddingTop: statusBarHeight + 'px' }">
<view class="nav-inner">
<view class="nav-back" @click="goBack">
<text class="nav-back-icon"></text>
<image class="back-icon" src="/static/ic_back2.png" mode="aspectFit" />
</view>
<text class="nav-title">{{ t('points.title') }}</text>
<view class="nav-placeholder" />
@ -172,9 +172,9 @@ loadRecords()
align-items: center;
justify-content: center;
}
.nav-back-icon {
font-size: 48rpx;
color: #333;
.back-icon {
width: 40rpx;
height: 40rpx;
}
.nav-title {
font-size: 34rpx;

View File

@ -1,10 +1,23 @@
<template>
<view class="privacy-page">
<view v-if="loading" class="loading">
<text>{{ t('common.loading') }}</text>
<!-- 自定义导航栏 -->
<view class="custom-nav" :style="{ paddingTop: statusBarHeight + 'px' }">
<view class="nav-inner">
<view class="nav-back" @click="goBack">
<image class="back-icon" src="/static/ic_back2.png" mode="aspectFit" />
</view>
<text class="nav-title">{{ t('privacy.title') }}</text>
<view class="nav-placeholder" />
</view>
</view>
<view v-else class="content">
<rich-text :nodes="content" />
<view class="page-body" :style="{ paddingTop: (statusBarHeight + 44) + 'px' }">
<view v-if="loading" class="loading">
<text>{{ t('common.loading') }}</text>
</view>
<view v-else class="content">
<rich-text :nodes="content" />
</view>
</view>
</view>
</template>
@ -16,45 +29,82 @@ import { getPrivacyPolicy } from '../../api/content.js'
const { t } = useI18n()
//
const statusBarHeight = ref(0)
try {
const sysInfo = uni.getSystemInfoSync()
statusBarHeight.value = sysInfo.statusBarHeight || 0
} catch (e) {}
const content = ref('')
const loading = ref(true)
function goBack() {
uni.navigateBack()
}
async function loadContent() {
loading.value = true
try {
const res = await getPrivacyPolicy()
content.value = res.data?.content ?? res.data ?? ''
} catch (e) {
/* 错误已统一处理 */
} finally {
loading.value = false
}
} catch (e) {}
finally { loading.value = false }
}
onMounted(() => {
loadContent()
})
onMounted(() => { loadContent() })
</script>
<style scoped>
.privacy-page {
min-height: 100vh;
background-color: #ffffff;
}
.custom-nav {
position: fixed;
top: 0; left: 0; right: 0;
z-index: 100;
background-color: #DBDBDB;
}
.nav-inner {
height: 44px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 16rpx;
}
.nav-back {
width: 60rpx;
height: 44px;
display: flex;
align-items: center;
justify-content: center;
}
.back-icon {
width: 40rpx;
height: 40rpx;
}
.nav-title {
font-size: 34rpx;
font-weight: 600;
color: #333;
}
.nav-placeholder {
width: 60rpx;
}
.page-body {
padding: 30rpx;
}
.loading {
display: flex;
align-items: center;
justify-content: center;
padding: 60rpx 0;
}
.loading text {
font-size: 28rpx;
color: #999;
}
.content {
font-size: 28rpx;
color: #333;