This commit is contained in:
parent
d43406380c
commit
539b58ea87
3
admin/components.d.ts
vendored
3
admin/components.d.ts
vendored
|
|
@ -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']
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,3 +10,4 @@ export * from './points'
|
|||
export * from './stamp'
|
||||
export * from './upload'
|
||||
export * from './user'
|
||||
export * from './testAccount'
|
||||
|
|
|
|||
18
admin/src/api/testAccount.ts
Normal file
18
admin/src/api/testAccount.ts
Normal 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)}`)
|
||||
}
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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' },
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
|
|
|||
123
admin/src/views/test-account/index.vue
Normal file
123
admin/src/views/test-account/index.vue
Normal 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>
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
12
backend/src/VendingMachine.Domain/Entities/TestAccount.cs
Normal file
12
backend/src/VendingMachine.Domain/Entities/TestAccount.cs
Normal 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;
|
||||
}
|
||||
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
461
backend/src/VendingMachine.Infrastructure/Data/Migrations/20260413055438_AddTestAccount.Designer.cs
generated
Normal file
461
backend/src/VendingMachine.Infrastructure/Data/Migrations/20260413055438_AddTestAccount.Designer.cs
generated
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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}";
|
||||
|
|
|
|||
|
|
@ -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和语言请求头,统一处理响应和错误
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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": "关于"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user