This commit is contained in:
2025-09-29 21:38:32 +08:00
parent ed26244cdb
commit 19117de6c8
182 changed files with 11590 additions and 2156 deletions

View File

@@ -2,6 +2,7 @@ import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
import Shell from '../views/Shell.vue' import Shell from '../views/Shell.vue'
const routes: RouteRecordRaw[] = [ const routes: RouteRecordRaw[] = [
{ path: '/admin/login', component: () => import('../views/admin/Login.vue') },
{ {
path: '/', path: '/',
component: Shell, component: Shell,
@@ -15,8 +16,9 @@ const routes: RouteRecordRaw[] = [
{ path: 'parts/templates', component: () => import('../views/parts/Templates.vue') }, { path: 'parts/templates', component: () => import('../views/parts/Templates.vue') },
{ path: 'consult', component: () => import('../views/consult/ConsultList.vue') } { path: 'consult', component: () => import('../views/consult/ConsultList.vue') }
,{ path: 'notice/list', component: () => import('../views/notice/NoticeList.vue') } ,{ path: 'notice/list', component: () => import('../views/notice/NoticeList.vue') }
,{ path: 'dict/units', component: () => import('../views/dict/Units.vue') }
,{ path: 'dict/categories', component: () => import('../views/dict/Categories.vue') } ,{ path: 'dict/categories', component: () => import('../views/dict/Categories.vue') }
,{ path: 'normal-admin/applications', component: () => import('../views/normal-admin/Applications.vue') }
] ]
} }
] ]

View File

@@ -14,8 +14,8 @@
<el-menu-item index="/parts/templates"><i class="el-icon-collection"></i><span>模板管理</span></el-menu-item> <el-menu-item index="/parts/templates"><i class="el-icon-collection"></i><span>模板管理</span></el-menu-item>
<el-menu-item index="/consult"><i class="el-icon-message"></i><span>咨询回复</span></el-menu-item> <el-menu-item index="/consult"><i class="el-icon-message"></i><span>咨询回复</span></el-menu-item>
<el-menu-item index="/notice/list"><i class="el-icon-notebook-1"></i><span>公告管理</span></el-menu-item> <el-menu-item index="/notice/list"><i class="el-icon-notebook-1"></i><span>公告管理</span></el-menu-item>
<el-menu-item index="/dict/units"><i class="el-icon-collection"></i><span>主单位</span></el-menu-item>
<el-menu-item index="/dict/categories"><i class="el-icon-collection-tag"></i><span>主类别</span></el-menu-item> <el-menu-item index="/dict/categories"><i class="el-icon-collection-tag"></i><span>主类别</span></el-menu-item>
<el-menu-item index="/normal-admin/applications"><i class="el-icon-user"></i><span>普通管理员审批</span></el-menu-item>
</el-menu> </el-menu>
</aside> </aside>
<main class="main"> <main class="main">

View File

@@ -0,0 +1,59 @@
<template>
<div class="login-wrap">
<el-card class="login-card">
<div class="title">平台管理员登录</div>
<el-form :model="form" @keyup.enter.native="onLogin">
<el-form-item label="用户名/手机号">
<el-input v-model="form.account" placeholder="用户名或手机号" />
</el-form-item>
<el-form-item label="密码">
<el-input v-model="form.password" type="password" placeholder="密码" />
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="loading" @click="onLogin">登录</el-button>
</el-form-item>
</el-form>
<div class="tips">说明登录成功后将把 ADMIN_ID/TOKEN 写入本地用于授权</div>
</el-card>
</div>
</template>
<script setup lang="ts">
import { reactive, ref } from 'vue'
import { ElMessage } from 'element-plus'
import { http } from '../../api/http'
import { useRouter } from 'vue-router'
const router = useRouter()
const form = reactive({ account: '', password: '' })
const loading = ref(false)
async function onLogin(){
if (!form.account.trim() || !form.password) return ElMessage.warning('请输入账号与密码')
loading.value = true
try {
const body:any = form.account.includes('@') || /^\d{5,}$/.test(form.account)
? { phone: form.account, password: form.password }
: { username: form.account, password: form.password }
const resp = await http.post('/api/admin/auth/login', body)
const admin = resp?.admin || {}
try {
if (admin?.adminId) localStorage.setItem('ADMIN_ID', String(admin.adminId))
if (resp?.token) localStorage.setItem('ADMIN_TOKEN', String(resp.token))
} catch {}
ElMessage.success('登录成功')
router.replace('/vip/system')
} catch (e:any) {
ElMessage.error(e.message || '登录失败')
} finally { loading.value=false }
}
</script>
<style scoped>
.login-wrap { height: 100vh; display:flex; align-items:center; justify-content:center; padding: 16px; }
.login-card { width: 380px; }
.title { font-weight: 800; font-size: 18px; margin-bottom: 12px; }
.tips { color:#888; font-size:12px; margin-top:8px; }
</style>

View File

@@ -0,0 +1,96 @@
<template>
<div class="page">
<div class="header">
<h2>普通管理员申请</h2>
</div>
<div class="panel" style="padding:12px; margin-bottom:12px;">
<el-form :inline="true" :model="query">
<el-form-item label="关键词">
<el-input v-model="query.kw" placeholder="姓名/手机号/邮箱" clearable />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="load">查询</el-button>
<el-button @click="reset">重置</el-button>
</el-form-item>
</el-form>
</div>
<el-table :data="rows" style="width:100%" :loading="loading" stripe>
<el-table-column type="index" width="60" label="#" />
<el-table-column prop="name" label="姓名" width="140" />
<el-table-column prop="phone" label="手机号" width="140" />
<el-table-column prop="email" label="邮箱" width="200" />
<el-table-column prop="shopId" label="店铺ID" width="100" />
<el-table-column prop="remark" label="备注" />
<el-table-column prop="createdAt" label="申请时间" width="180">
<template #default="{row}">{{ formatDate(row.createdAt) }}</template>
</el-table-column>
<el-table-column label="操作" width="220" fixed="right">
<template #default="{row}">
<el-button type="primary" size="small" @click="approve(row.userId)">通过</el-button>
<el-button type="danger" size="small" @click="openReject(row.userId)">驳回</el-button>
</template>
</el-table-column>
</el-table>
<div class="pager">
<el-pagination
background
layout="prev, pager, next, jumper, ->, total"
:total="total"
:page-size="query.size"
:current-page="query.page"
@current-change="onPage"
/>
</div>
<el-dialog v-model="reject.visible" title="驳回原因" width="420px">
<el-input type="textarea" v-model="reject.reason" placeholder="请输入驳回原因" :rows="4" />
<template #footer>
<el-button @click="reject.visible=false">取消</el-button>
<el-button type="danger" @click="doReject" :loading="reject.loading">确认驳回</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { reactive, ref, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { get, post } from '../../api/http'
const query = reactive({ kw: '', page: 1, size: 20 })
const rows = ref<any[]>([])
const total = ref(0)
const loading = ref(false)
const reject = reactive({ visible: false, userId: 0, reason: '', loading: false })
function formatDate(v?: string) { if (!v) return '-'; try { const d = new Date(v); if (Number.isNaN(d.getTime())) return '-'; return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')} ${String(d.getHours()).padStart(2,'0')}:${String(d.getMinutes()).padStart(2,'0')}` } catch { return '-' } }
async function load(){
loading.value = true
try {
const res:any = await get('/api/admin/normal-admin/applications', { kw: query.kw, page: query.page, size: query.size })
rows.value = Array.isArray(res?.list) ? res.list : []
total.value = Number(res?.total || rows.value.length)
} catch (e:any) { ElMessage.error(e.message || '加载失败') } finally { loading.value=false }
}
function reset(){ query.kw=''; query.page=1; load() }
function onPage(p:number){ query.page=p; load() }
async function approve(userId:number){
try { await post(`/api/admin/normal-admin/applications/${userId}/approve`, {}) ; ElMessage.success('已通过'); load() } catch(e:any){ ElMessage.error(e.message||'操作失败') }
}
function openReject(userId:number){ reject.visible=true; reject.userId=userId; reject.reason='' }
async function doReject(){ if (!reject.reason.trim()) return ElMessage.warning('请输入驳回原因'); reject.loading=true; try { await post(`/api/admin/normal-admin/applications/${reject.userId}/reject`, { remark: reject.reason }); reject.visible=false; ElMessage.success('已驳回'); load() } catch(e:any){ ElMessage.error(e.message||'操作失败') } finally { reject.loading=false } }
onMounted(load)
</script>
<style scoped>
.page { padding: 16px; }
.header { display:flex; align-items:center; justify-content: space-between; margin-bottom: 12px; }
.pager { padding: 12px; display:flex; justify-content:flex-end; }
</style>

View File

@@ -23,31 +23,37 @@
<el-tag :type="row.status===1?'success':'info'">{{ row.status===1?'启用':'停用' }}</el-tag> <el-tag :type="row.status===1?'success':'info'">{{ row.status===1?'启用':'停用' }}</el-tag>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="操作" width="200"> <el-table-column label="操作" width="220">
<template #default="{row}"> <template #default="{row}">
<el-button type="primary" text @click="openEdit(row)">编辑</el-button> <el-button type="primary" text @click="openView(row)">查看</el-button>
<el-divider direction="vertical" />
<el-popconfirm title="确认删除该模板?此操作不可恢复" @confirm="doDelete(row)">
<template #reference>
<el-button type="danger" text>删除</el-button>
</template>
</el-popconfirm>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
<el-dialog v-model="dlg.visible" :title="dlg.id? '编辑模板':'新建模板'" width="720"> <el-dialog v-model="dlg.visible" :title="dlg.id? '查看模板' : '新建模板'" width="720">
<el-form :model="form" label-width="100px"> <el-form :model="form" label-width="100px">
<el-form-item label="分类"> <el-form-item label="分类">
<el-select v-model="form.categoryId" placeholder="选择分类"> <el-select v-model="form.categoryId" placeholder="选择分类" :disabled="!!dlg.id">
<el-option v-for="c in categories" :key="c.id" :label="c.name" :value="c.id" /> <el-option v-for="c in categories" :key="c.id" :label="c.name" :value="c.id" />
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="模板名"> <el-form-item label="模板名">
<el-input v-model="form.name" maxlength="120" /> <el-input v-model="form.name" maxlength="120" :disabled="!!dlg.id" />
</el-form-item> </el-form-item>
<el-form-item label="型号规则"> <el-form-item label="型号规则">
<el-input v-model="form.modelRule" placeholder="可填备注或正则" /> <el-input v-model="form.modelRule" placeholder="可填备注或正则" :disabled="!!dlg.id" />
</el-form-item> </el-form-item>
<el-form-item label="状态"> <el-form-item label="状态">
<el-switch v-model="form.status" :active-value="1" :inactive-value="0" /> <el-switch v-model="form.status" :active-value="1" :inactive-value="0" :disabled="!!dlg.id" />
</el-form-item> </el-form-item>
<el-divider>参数字段</el-divider> <el-divider>参数字段</el-divider>
<div> <div v-if="!dlg.id">
<el-button size="small" @click="addParam()">新增字段</el-button> <el-button size="small" @click="addParam()">新增字段</el-button>
</div> </div>
<el-table :data="form.params" size="small" style="width:100%;margin-top:10px"> <el-table :data="form.params" size="small" style="width:100%;margin-top:10px">
@@ -55,14 +61,14 @@
<template #default="{ $index }">{{ $index+1 }}</template> <template #default="{ $index }">{{ $index+1 }}</template>
</el-table-column> </el-table-column>
<el-table-column label="键"> <el-table-column label="键">
<template #default="{row}"><el-input v-model="row.fieldKey" placeholder="英文字母/下划线" /></template> <template #default="{row}"><el-input v-model="row.fieldKey" placeholder="英文字母/下划线" :disabled="!!dlg.id" /></template>
</el-table-column> </el-table-column>
<el-table-column label="名称"> <el-table-column label="名称">
<template #default="{row}"><el-input v-model="row.fieldLabel" /></template> <template #default="{row}"><el-input v-model="row.fieldLabel" :disabled="!!dlg.id" /></template>
</el-table-column> </el-table-column>
<el-table-column label="类型" width="120"> <el-table-column label="类型" width="120">
<template #default="{row}"> <template #default="{row}">
<el-select v-model="row.type" style="width:110px"> <el-select v-model="row.type" style="width:110px" :disabled="!!dlg.id">
<el-option label="string" value="string" /> <el-option label="string" value="string" />
<el-option label="number" value="number" /> <el-option label="number" value="number" />
<el-option label="boolean" value="boolean" /> <el-option label="boolean" value="boolean" />
@@ -72,28 +78,31 @@
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="必填" width="80"> <el-table-column label="必填" width="80">
<template #default="{row}"><el-switch v-model="row.required" /></template> <template #default="{row}"><el-switch v-model="row.required" :disabled="!!dlg.id" /></template>
</el-table-column> </el-table-column>
<el-table-column label="单位" width="120"> <el-table-column label="单位" width="120">
<template #default="{row}"><el-input v-model="row.unit" /></template> <template #default="{row}"><el-input v-model="row.unit" :disabled="!!dlg.id" /></template>
</el-table-column> </el-table-column>
<el-table-column label="枚举项"> <el-table-column label="枚举项">
<template #default="{row}"><el-input v-model="row.enumOptionsText" placeholder="逗号分隔type=enum" /></template> <template #default="{row}"><el-input v-model="row.enumOptionsText" placeholder="逗号分隔type=enum" :disabled="!!dlg.id" /></template>
</el-table-column> </el-table-column>
<el-table-column label="检索" width="80"> <el-table-column label="检索" width="80">
<template #default="{row}"><el-switch v-model="row.searchable" /></template> <template #default="{row}"><el-switch v-model="row.searchable" :disabled="!!dlg.id" /></template>
</el-table-column> </el-table-column>
<el-table-column label="去重" width="80"> <el-table-column label="去重" width="80">
<template #default="{row}"><el-switch v-model="row.dedupeParticipate" /></template> <template #default="{row}"><el-switch v-model="row.dedupeParticipate" :disabled="!!dlg.id" /></template>
</el-table-column> </el-table-column>
<el-table-column label="操作" width="90"> <el-table-column label="操作" width="90">
<template #default="{ $index }"><el-button text type="danger" @click="removeParam($index)">删除</el-button></template> <template #default="{ $index }">
<el-button v-if="!dlg.id" text type="danger" @click="removeParam($index)">删除</el-button>
<span v-else></span>
</template>
</el-table-column> </el-table-column>
</el-table> </el-table>
</el-form> </el-form>
<template #footer> <template #footer>
<el-button @click="dlg.visible=false">取消</el-button> <el-button @click="dlg.visible=false">{{ dlg.id? '关闭':'取消' }}</el-button>
<el-button type="primary" @click="save()">保存</el-button> <el-button v-if="!dlg.id" type="primary" @click="save()">保存</el-button>
</template> </template>
</el-dialog> </el-dialog>
</div> </div>
@@ -122,11 +131,7 @@ function loadCategories() {
}) })
} }
function openCreate() { function openView(row:any) {
dlg.visible = true; dlg.id = 0
Object.assign(form, { id:0, categoryId: undefined, name:'', modelRule:'', status:1, params:[] })
}
function openEdit(row:any) {
dlg.visible = true; dlg.id = row.id dlg.visible = true; dlg.id = row.id
http.get(`/api/admin/part-templates/${row.id}`).then(res => { http.get(`/api/admin/part-templates/${row.id}`).then(res => {
const d = res.data const d = res.data
@@ -135,14 +140,19 @@ function openEdit(row:any) {
}) })
} }
function openCreate() {
dlg.visible = true; dlg.id = 0
Object.assign(form, { id: 0, categoryId: undefined, name: '', modelRule: '', status: 1, params: [] })
}
function addParam() { function addParam() {
form.params.push({ fieldKey:'', fieldLabel:'', type:'string', required:false, unit:'', enumOptionsText:'', searchable:false, dedupeParticipate:false, sortOrder:0 }) form.params.push({ fieldKey:'', fieldLabel:'', type:'string', required:false, unit:'', enumOptionsText:'', searchable:false, dedupeParticipate:false, sortOrder:0 })
} }
function removeParam(i:number) {
form.params.splice(i,1) function removeParam(i:number) { form.params.splice(i,1) }
}
function save() { function save() {
// 前端校验fieldKey/fieldLabel/类型/重复 // 校验
const seen = new Set<string>() const seen = new Set<string>()
for (const p of form.params) { for (const p of form.params) {
const key = String(p.fieldKey||'').trim() const key = String(p.fieldKey||'').trim()
@@ -157,9 +167,19 @@ function save() {
const payload:any = { categoryId: form.categoryId, name: form.name, modelRule: form.modelRule, status: form.status, const payload:any = { categoryId: form.categoryId, name: form.name, modelRule: form.modelRule, status: form.status,
params: form.params.map((p:any)=>({ fieldKey:p.fieldKey, fieldLabel:p.fieldLabel, type:p.type, required:p.required, unit:p.unit, params: form.params.map((p:any)=>({ fieldKey:p.fieldKey, fieldLabel:p.fieldLabel, type:p.type, required:p.required, unit:p.unit,
enumOptions: (p.enumOptionsText||'').split(',').map((s:string)=>s.trim()).filter((s:string)=>s), searchable:p.searchable, dedupeParticipate:p.dedupeParticipate, sortOrder:p.sortOrder })) } enumOptions: (p.enumOptionsText||'').split(',').map((s:string)=>s.trim()).filter((s:string)=>s), searchable:p.searchable, dedupeParticipate:p.dedupeParticipate, sortOrder:p.sortOrder })) }
const req = dlg.id ? http.put(`/api/admin/part-templates/${dlg.id}`, { ...payload, deleteAllRelatedProductsAndSubmissions: true }) http.post('/api/admin/part-templates', payload).then(()=>{
: http.post('/api/admin/part-templates', payload) ElMessage.success('创建成功'); dlg.visible=false; load()
req.then(()=>{ ElMessage.success('保存成功'); dlg.visible=false; load() }) })
}
function doDelete(row:any) {
// 默认非强制删除;如需强制,可加 { params: { force: true } }
http.delete(`/api/admin/part-templates/${row.id}`).then(()=>{
ElMessage.success('已隐藏(可通过后台强制删除彻底清理)')
load()
}).catch((err:any)=>{
ElMessage.error(err?.message || '删除失败')
})
} }
onMounted(()=>{ loadCategories(); load() }) onMounted(()=>{ loadCategories(); load() })

Binary file not shown.

After

Width:  |  Height:  |  Size: 233 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 459 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

View File

@@ -156,7 +156,7 @@ CREATE TABLE IF NOT EXISTS products (
user_id BIGINT UNSIGNED NOT NULL, user_id BIGINT UNSIGNED NOT NULL,
name VARCHAR(120) NOT NULL, name VARCHAR(120) NOT NULL,
category_id BIGINT UNSIGNED NULL, category_id BIGINT UNSIGNED NULL,
unit_id BIGINT UNSIGNED NOT NULL, -- unit_id 已移除
brand VARCHAR(64) NULL, brand VARCHAR(64) NULL,
model VARCHAR(64) NULL, model VARCHAR(64) NULL,
spec VARCHAR(128) NULL, spec VARCHAR(128) NULL,
@@ -175,12 +175,12 @@ CREATE TABLE IF NOT EXISTS products (
UNIQUE KEY ux_products_shop_barcode (shop_id, barcode), UNIQUE KEY ux_products_shop_barcode (shop_id, barcode),
KEY idx_products_shop (shop_id), KEY idx_products_shop (shop_id),
KEY idx_products_category (category_id), KEY idx_products_category (category_id),
KEY idx_products_unit (unit_id), -- KEY idx_products_unit (unit_id),
FULLTEXT KEY ft_products_search (name, brand, model, spec, search_text), FULLTEXT KEY ft_products_search (name, brand, model, spec, search_text),
CONSTRAINT fk_products_shop FOREIGN KEY (shop_id) REFERENCES shops(id), CONSTRAINT fk_products_shop FOREIGN KEY (shop_id) REFERENCES shops(id),
CONSTRAINT fk_products_user FOREIGN KEY (user_id) REFERENCES users(id), CONSTRAINT fk_products_user FOREIGN KEY (user_id) REFERENCES users(id),
CONSTRAINT fk_products_category FOREIGN KEY (category_id) REFERENCES product_categories(id), CONSTRAINT fk_products_category FOREIGN KEY (category_id) REFERENCES product_categories(id),
CONSTRAINT fk_products_unit FOREIGN KEY (unit_id) REFERENCES product_units(id), -- CONSTRAINT fk_products_unit FOREIGN KEY (unit_id) REFERENCES product_units(id),
CONSTRAINT fk_products_globalsku FOREIGN KEY (global_sku_id) REFERENCES global_skus(id), CONSTRAINT fk_products_globalsku FOREIGN KEY (global_sku_id) REFERENCES global_skus(id),
CONSTRAINT ck_products_safe_range CHECK (safe_min IS NULL OR safe_max IS NULL OR safe_min <= safe_max) CONSTRAINT ck_products_safe_range CHECK (safe_min IS NULL OR safe_max IS NULL OR safe_min <= safe_max)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='商品'; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='商品';

View File

@@ -71,11 +71,7 @@ public class AdminDictController {
public ResponseEntity<?> deleteUnit(@PathVariable("id") Long id) { public ResponseEntity<?> deleteUnit(@PathVariable("id") Long id) {
ProductUnit u = unitRepository.findById(id).orElse(null); ProductUnit u = unitRepository.findById(id).orElse(null);
if (u == null || !u.getShopId().equals(defaults.getDictShopId())) return ResponseEntity.status(404).body(Map.of("message","not found")); if (u == null || !u.getShopId().equals(defaults.getDictShopId())) return ResponseEntity.status(404).body(Map.of("message","not found"));
// 引用保护:若有商品使用该单位,阻止删除 // 按新方案:移除对 products.unit_id 的引用校验(该字段已移除)
Long cnt = jdbcTemplate.queryForObject("SELECT COUNT(*) FROM products WHERE unit_id=?", Long.class, id);
if (cnt != null && cnt > 0) {
return ResponseEntity.status(409).body(Map.of("message","存在引用,无法删除"));
}
unitRepository.deleteById(id); unitRepository.deleteById(id);
return ResponseEntity.ok().build(); return ResponseEntity.ok().build();
} }
@@ -121,12 +117,15 @@ public class AdminDictController {
public ResponseEntity<?> deleteCategory(@PathVariable("id") Long id) { public ResponseEntity<?> deleteCategory(@PathVariable("id") Long id) {
ProductCategory c = categoryRepository.findById(id).orElse(null); ProductCategory c = categoryRepository.findById(id).orElse(null);
if (c == null || !c.getShopId().equals(defaults.getDictShopId())) return ResponseEntity.status(404).body(Map.of("message","not found")); if (c == null || !c.getShopId().equals(defaults.getDictShopId())) return ResponseEntity.status(404).body(Map.of("message","not found"));
// 子类与引用保护 // 平台管理员二次确认可在拦截器或前端完成;此处执行软删级联
Long child = jdbcTemplate.queryForObject("SELECT COUNT(*) FROM product_categories WHERE parent_id=?", Long.class, id); // 1) 软删分类
if (child != null && child > 0) return ResponseEntity.status(409).body(Map.of("message","存在子类,无法删除")); jdbcTemplate.update("UPDATE product_categories SET deleted_at=NOW(), updated_at=NOW() WHERE id=? AND deleted_at IS NULL", id);
Long cnt = jdbcTemplate.queryForObject("SELECT COUNT(*) FROM products WHERE category_id=?", Long.class, id); // 2) 软删分类下模板(使用 deleted_at 统一标记)
if (cnt != null && cnt > 0) return ResponseEntity.status(409).body(Map.of("message","存在引用,无法删除")); jdbcTemplate.update("UPDATE part_templates SET deleted_at=NOW(), updated_at=NOW() WHERE category_id=? AND (deleted_at IS NULL)", id);
categoryRepository.deleteById(id); // 3) 软删该分类下的所有商品:包括通过模板创建的与直接挂分类的
jdbcTemplate.update("UPDATE products SET deleted_at=NOW(), updated_at=NOW() WHERE (category_id=? OR template_id IN (SELECT id FROM part_templates WHERE category_id=?)) AND deleted_at IS NULL", id, id);
// 4) 软删该分类下的所有配件提交:包含直接指向分类的与指向该分类下模板的
jdbcTemplate.update("UPDATE part_submissions SET deleted_at=NOW(), updated_at=NOW() WHERE (category_id=? OR template_id IN (SELECT id FROM part_templates WHERE category_id=?)) AND deleted_at IS NULL", id, id);
return ResponseEntity.ok().build(); return ResponseEntity.ok().build();
} }
} }

View File

@@ -21,16 +21,20 @@ public class EmailAuthService {
private final com.example.demo.common.ShopDefaultsProperties shopDefaults; private final com.example.demo.common.ShopDefaultsProperties shopDefaults;
private final EmailSenderService emailSender; private final EmailSenderService emailSender;
private final com.example.demo.common.DefaultSeedService defaultSeedService;
public EmailAuthService(JdbcTemplate jdbcTemplate, public EmailAuthService(JdbcTemplate jdbcTemplate,
JwtService jwtService, JwtService jwtService,
JwtProperties jwtProps, JwtProperties jwtProps,
com.example.demo.common.ShopDefaultsProperties shopDefaults, com.example.demo.common.ShopDefaultsProperties shopDefaults,
EmailSenderService emailSender) { EmailSenderService emailSender,
com.example.demo.common.DefaultSeedService defaultSeedService) {
this.jdbcTemplate = jdbcTemplate; this.jdbcTemplate = jdbcTemplate;
this.jwtService = jwtService; this.jwtService = jwtService;
this.jwtProps = jwtProps; this.jwtProps = jwtProps;
this.shopDefaults = shopDefaults; this.shopDefaults = shopDefaults;
this.emailSender = emailSender; this.emailSender = emailSender;
this.defaultSeedService = defaultSeedService;
} }
public static class SendCodeRequest { public String email; public String scene; } public static class SendCodeRequest { public String email; public String scene; }
@@ -205,6 +209,9 @@ public class EmailAuthService {
Number userGenId = userKey.getKey(); Number userGenId = userKey.getKey();
if (userGenId == null) throw new IllegalStateException("创建用户失败"); if (userGenId == null) throw new IllegalStateException("创建用户失败");
userId = userGenId.longValue(); userId = userGenId.longValue();
// 初始化默认客户/供应商(幂等)
defaultSeedService.initializeForShop(shopId, userId);
} }
String token = jwtService.signToken(userId, shopId, null, "email_otp", email); String token = jwtService.signToken(userId, shopId, null, "email_otp", email);

View File

@@ -6,6 +6,7 @@ import org.springframework.web.bind.annotation.*;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.LinkedHashMap;
@RestController @RestController
@RequestMapping("/api/normal-admin") @RequestMapping("/api/normal-admin")
@@ -27,6 +28,11 @@ public class NormalAdminApplyController {
} else { sidFinal = shopId; } } else { sidFinal = shopId; }
// 校验 VIP根据配置可选 // 校验 VIP根据配置可选
boolean requireVip = true; // 默认要求VIP有效 boolean requireVip = true; // 默认要求VIP有效
try {
String v = jdbc.query("SELECT value FROM system_parameters WHERE `key`='normalAdmin.requiredVipActive' ORDER BY id DESC LIMIT 1",
rs -> rs.next() ? rs.getString(1) : null);
if (v != null) { v = v.trim(); if (v.startsWith("\"") && v.endsWith("\"")) v = v.substring(1, v.length()-1); requireVip = ("true".equalsIgnoreCase(v) || "1".equals(v)); }
} catch (Exception ignored) {}
Integer vipOk = jdbc.query( Integer vipOk = jdbc.query(
"SELECT CASE WHEN (is_vip=1 AND status=1 AND (expire_at IS NULL OR expire_at>NOW())) THEN 1 ELSE 0 END FROM vip_users WHERE user_id=? AND shop_id=? ORDER BY id DESC LIMIT 1", "SELECT CASE WHEN (is_vip=1 AND status=1 AND (expire_at IS NULL OR expire_at>NOW())) THEN 1 ELSE 0 END FROM vip_users WHERE user_id=? AND shop_id=? ORDER BY id DESC LIMIT 1",
ps -> { ps.setLong(1, userId); ps.setLong(2, sidFinal); }, ps -> { ps.setLong(1, userId); ps.setLong(2, sidFinal); },
@@ -41,7 +47,12 @@ public class NormalAdminApplyController {
ps -> { ps.setLong(1, sidFinal); ps.setLong(2, userId); ps.setString(3, "apply"); ps.setString(4, remark); }); ps -> { ps.setLong(1, sidFinal); ps.setLong(2, userId); ps.setString(3, "apply"); ps.setString(4, remark); });
// 是否自动通过 // 是否自动通过
boolean autoApprove = false; // 默认false后续接入 system_parameters boolean autoApprove = false;
try {
String v = jdbc.query("SELECT value FROM system_parameters WHERE `key`='normalAdmin.autoApprove' ORDER BY id DESC LIMIT 1",
rs -> rs.next() ? rs.getString(1) : null);
if (v != null) { v = v.trim(); if (v.startsWith("\"") && v.endsWith("\"")) v = v.substring(1, v.length()-1); autoApprove = ("true".equalsIgnoreCase(v) || "1".equals(v)); }
} catch (Exception ignored) {}
if (autoApprove) { if (autoApprove) {
// 将角色变更为 normal_admin 并写入 approve 审计 // 将角色变更为 normal_admin 并写入 approve 审计
String prev = jdbc.query("SELECT role FROM users WHERE id=?", ps -> ps.setLong(1, userId), rs -> rs.next()? rs.getString(1): null); String prev = jdbc.query("SELECT role FROM users WHERE id=?", ps -> ps.setLong(1, userId), rs -> rs.next()? rs.getString(1): null);
@@ -52,6 +63,62 @@ public class NormalAdminApplyController {
return ResponseEntity.ok(Map.of("ok", true)); return ResponseEntity.ok(Map.of("ok", true));
} }
@GetMapping("/application/status")
public ResponseEntity<?> myApplicationStatus(@RequestHeader(name = "X-User-Id") long userId) {
try {
Map<String, Object> out = new LinkedHashMap<>();
// 当前角色
String role = null;
try {
role = jdbc.query("SELECT role FROM users WHERE id=? LIMIT 1",
ps -> ps.setLong(1, userId), rs -> rs.next() ? rs.getString(1) : null);
} catch (Exception ignored) {}
boolean isNormalAdmin = role != null && "normal_admin".equalsIgnoreCase(role.trim());
// 最近一次审计动作
Map<String, Object> last = null;
try {
last = jdbc.query(
"SELECT action, created_at AS createdAt, remark FROM normal_admin_audits WHERE user_id=? ORDER BY created_at DESC LIMIT 1",
ps -> ps.setLong(1, userId),
rs -> {
if (!rs.next()) return null;
Map<String,Object> m = new LinkedHashMap<>();
m.put("action", rs.getString("action"));
m.put("createdAt", rs.getTimestamp("createdAt"));
m.put("remark", rs.getString("remark"));
return m;
}
);
} catch (Exception ignored) {}
String applicationStatus = "none";
if (isNormalAdmin) {
applicationStatus = "approved";
} else if (last != null) {
String action = (String) last.get("action");
if ("apply".equalsIgnoreCase(action)) applicationStatus = "pending";
else if ("approve".equalsIgnoreCase(action)) applicationStatus = "approved";
else if ("reject".equalsIgnoreCase(action)) applicationStatus = "rejected";
else if ("revoke".equalsIgnoreCase(action)) applicationStatus = "revoked";
}
out.put("isNormalAdmin", isNormalAdmin);
out.put("applicationStatus", applicationStatus);
if (last != null) {
out.put("lastAction", last.get("action"));
out.put("lastActionAt", last.get("createdAt"));
out.put("lastRemark", last.get("remark"));
}
return ResponseEntity.ok(out);
} catch (Exception e) {
Map<String,Object> fallback = new LinkedHashMap<>();
fallback.put("isNormalAdmin", false);
fallback.put("applicationStatus", "none");
return ResponseEntity.ok(fallback);
}
}
} }

View File

@@ -20,6 +20,7 @@ public class RegisterService {
private final JwtProperties jwtProps; private final JwtProperties jwtProps;
private final ShopDefaultsProperties shopDefaults; private final ShopDefaultsProperties shopDefaults;
private AppDefaultsProperties appDefaults; private AppDefaultsProperties appDefaults;
private com.example.demo.common.DefaultSeedService defaultSeedService;
public RegisterService(JdbcTemplate jdbcTemplate, public RegisterService(JdbcTemplate jdbcTemplate,
JwtService jwtService, JwtService jwtService,
@@ -36,6 +37,11 @@ public class RegisterService {
this.appDefaults = appDefaults; this.appDefaults = appDefaults;
} }
@Autowired
public void setDefaultSeedService(com.example.demo.common.DefaultSeedService defaultSeedService) {
this.defaultSeedService = defaultSeedService;
}
private String hashPassword(String raw) { private String hashPassword(String raw) {
try { try {
return org.springframework.security.crypto.bcrypt.BCrypt.hashpw(raw, org.springframework.security.crypto.bcrypt.BCrypt.gensalt(10)); return org.springframework.security.crypto.bcrypt.BCrypt.hashpw(raw, org.springframework.security.crypto.bcrypt.BCrypt.gensalt(10));
@@ -108,6 +114,11 @@ public class RegisterService {
// 3) 创建默认账户(现金/银行存款/微信) // 3) 创建默认账户(现金/银行存款/微信)
createDefaultAccounts(shopId, userId); createDefaultAccounts(shopId, userId);
// 4) 初始化默认客户/供应商(幂等)
if (defaultSeedService != null) {
defaultSeedService.initializeForShop(shopId, userId);
}
} }
String token = jwtService.signToken(userId, shopId, phone, "register"); String token = jwtService.signToken(userId, shopId, phone, "register");

View File

@@ -18,6 +18,10 @@ public class AppDefaultsProperties {
private String accountWechatName = "微信"; private String accountWechatName = "微信";
private String accountAlipayName = "支付宝"; private String accountAlipayName = "支付宝";
// 默认往来单位名称(配置化,避免硬编码)
private String customerName = "散客";
private String supplierName = "默认供应商";
public Long getShopId() { return shopId; } public Long getShopId() { return shopId; }
public void setShopId(Long shopId) { this.shopId = shopId; } public void setShopId(Long shopId) { this.shopId = shopId; }
public Long getUserId() { return userId; } public Long getUserId() { return userId; }
@@ -34,6 +38,12 @@ public class AppDefaultsProperties {
public void setAccountWechatName(String accountWechatName) { this.accountWechatName = accountWechatName; } public void setAccountWechatName(String accountWechatName) { this.accountWechatName = accountWechatName; }
public String getAccountAlipayName() { return accountAlipayName; } public String getAccountAlipayName() { return accountAlipayName; }
public void setAccountAlipayName(String accountAlipayName) { this.accountAlipayName = accountAlipayName; } public void setAccountAlipayName(String accountAlipayName) { this.accountAlipayName = accountAlipayName; }
public String getCustomerName() { return customerName; }
public void setCustomerName(String customerName) { this.customerName = customerName; }
public String getSupplierName() { return supplierName; }
public void setSupplierName(String supplierName) { this.supplierName = supplierName; }
} }

View File

@@ -0,0 +1,41 @@
package com.example.demo.common;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class DefaultSeedService {
private final JdbcTemplate jdbcTemplate;
private final AppDefaultsProperties appDefaults;
public DefaultSeedService(JdbcTemplate jdbcTemplate, AppDefaultsProperties appDefaults) {
this.jdbcTemplate = jdbcTemplate;
this.appDefaults = appDefaults;
}
/**
* 幂等初始化:为新店铺创建默认客户/供应商(若不存在)。
*/
@Transactional
public void initializeForShop(Long shopId, Long userId) {
if (shopId == null || userId == null) return;
// 默认客户
jdbcTemplate.update(
"INSERT INTO customers (shop_id,user_id,name,price_level,status,created_at,updated_at) " +
"SELECT ?, ?, ?, 'retail', 1, NOW(), NOW() FROM DUAL " +
"WHERE NOT EXISTS (SELECT 1 FROM customers WHERE shop_id=? AND name=?)",
shopId, userId, appDefaults.getCustomerName(), shopId, appDefaults.getCustomerName()
);
// 默认供应商
jdbcTemplate.update(
"INSERT INTO suppliers (shop_id,user_id,name,status,created_at,updated_at) " +
"SELECT ?, ?, ?, 1, NOW(), NOW() FROM DUAL " +
"WHERE NOT EXISTS (SELECT 1 FROM suppliers WHERE shop_id=? AND name=?)",
shopId, userId, appDefaults.getSupplierName(), shopId, appDefaults.getSupplierName()
);
}
}

View File

@@ -59,7 +59,16 @@ public class NormalAdminAuthInterceptor implements HandlerInterceptor {
} }
// 可选校验VIP 有效 // 可选校验VIP 有效
boolean requireVip = Boolean.parseBoolean(String.valueOf(System.getenv().getOrDefault("NORMAL_ADMIN_REQUIRE_VIP_ACTIVE", "true"))); boolean requireVip;
try {
String v = jdbcTemplate.query("SELECT value FROM system_parameters WHERE `key`='normalAdmin.requiredVipActive' ORDER BY id DESC LIMIT 1",
rs -> rs.next() ? rs.getString(1) : null);
if (v == null) requireVip = true; else {
v = v.trim();
if (v.startsWith("\"") && v.endsWith("\"")) v = v.substring(1, v.length()-1);
requireVip = "true".equalsIgnoreCase(v) || "1".equals(v);
}
} catch (Exception e) { requireVip = true; }
if (requireVip) { if (requireVip) {
Integer vipOk = jdbcTemplate.query( Integer vipOk = jdbcTemplate.query(
"SELECT CASE WHEN (is_vip=1 AND status=1 AND (expire_at IS NULL OR expire_at>NOW())) THEN 1 ELSE 0 END FROM vip_users WHERE user_id=? AND shop_id=? ORDER BY id DESC LIMIT 1", "SELECT CASE WHEN (is_vip=1 AND status=1 AND (expire_at IS NULL OR expire_at>NOW())) THEN 1 ELSE 0 END FROM vip_users WHERE user_id=? AND shop_id=? ORDER BY id DESC LIMIT 1",

View File

@@ -4,6 +4,7 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistration; import org.springframework.web.servlet.config.annotation.InterceptorRegistration;
import org.springframework.lang.NonNull; import org.springframework.lang.NonNull;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration @Configuration
@@ -29,6 +30,19 @@ public class WebConfig implements WebMvcConfigurer {
InterceptorRegistration nr = registry.addInterceptor(normalAdminAuthInterceptor); InterceptorRegistration nr = registry.addInterceptor(normalAdminAuthInterceptor);
nr.addPathPatterns("/api/normal-admin/parts/**"); nr.addPathPatterns("/api/normal-admin/parts/**");
} }
@Override
public void addResourceHandlers(@NonNull ResourceHandlerRegistry registry) {
// 将 /static/** 映射到前端静态资源目录(开发时)与 classpath 静态目录(部署时)
String userDir = System.getProperty("user.dir");
String frontendStatic = userDir + java.io.File.separator + "frontend" + java.io.File.separator + "static" + java.io.File.separator;
registry.addResourceHandler("/static/**")
.addResourceLocations(
"file:" + frontendStatic,
"classpath:/static/",
"classpath:/public/"
);
}
} }

View File

@@ -27,14 +27,18 @@ public class OrderService {
private final JdbcTemplate jdbcTemplate; private final JdbcTemplate jdbcTemplate;
private final ProductPriceRepository productPriceRepository; private final ProductPriceRepository productPriceRepository;
private final com.example.demo.common.AppDefaultsProperties appDefaults;
public OrderService(InventoryRepository inventoryRepository, public OrderService(InventoryRepository inventoryRepository,
JdbcTemplate jdbcTemplate, JdbcTemplate jdbcTemplate,
AccountDefaultsProperties accountDefaults, AccountDefaultsProperties accountDefaults,
ProductPriceRepository productPriceRepository) { ProductPriceRepository productPriceRepository,
com.example.demo.common.AppDefaultsProperties appDefaults) {
this.inventoryRepository = inventoryRepository; this.inventoryRepository = inventoryRepository;
this.jdbcTemplate = jdbcTemplate; this.jdbcTemplate = jdbcTemplate;
this.accountDefaults = accountDefaults; this.accountDefaults = accountDefaults;
this.productPriceRepository = productPriceRepository; this.productPriceRepository = productPriceRepository;
this.appDefaults = appDefaults;
} }
@Transactional @Transactional
@@ -134,8 +138,12 @@ public class OrderService {
"VALUES (?,?,?,?,'approved', ?, 0, ?, NOW(), NOW())"; "VALUES (?,?,?,?,'approved', ?, 0, ?, NOW(), NOW())";
} }
Long customerId = req.customerId; final Long customerId = (isSalesHead && req.customerId == null)
Long supplierId = req.supplierId; ? resolveOrCreateDefaultCustomer(shopId, userId)
: req.customerId;
final Long supplierId = (isPurchaseHead && req.supplierId == null)
? resolveOrCreateDefaultSupplier(shopId, userId)
: req.supplierId;
jdbcTemplate.update(con -> { jdbcTemplate.update(con -> {
java.sql.PreparedStatement ps = con.prepareStatement(headSql, new String[]{"id"}); java.sql.PreparedStatement ps = con.prepareStatement(headSql, new String[]{"id"});
int idx = 1; int idx = 1;
@@ -188,6 +196,28 @@ public class OrderService {
return new OrderDtos.CreateOrderResponse(orderId, orderNo); return new OrderDtos.CreateOrderResponse(orderId, orderNo);
} }
private Long resolveOrCreateDefaultCustomer(Long shopId, Long userId) {
String name = appDefaults.getCustomerName();
java.util.List<Long> ids = jdbcTemplate.query("SELECT id FROM customers WHERE shop_id=? AND name=? LIMIT 1", (rs,rn)->rs.getLong(1), shopId, name);
if (!ids.isEmpty()) return ids.get(0);
jdbcTemplate.update("INSERT INTO customers (shop_id,user_id,name,price_level,status,created_at,updated_at) VALUES (?,?,?,'retail',1,NOW(),NOW())",
shopId, userId, name);
ids = jdbcTemplate.query("SELECT id FROM customers WHERE shop_id=? AND name=? LIMIT 1", (rs,rn)->rs.getLong(1), shopId, name);
if (!ids.isEmpty()) return ids.get(0);
throw new IllegalStateException("默认客户创建失败");
}
private Long resolveOrCreateDefaultSupplier(Long shopId, Long userId) {
String name = appDefaults.getSupplierName();
java.util.List<Long> ids = jdbcTemplate.query("SELECT id FROM suppliers WHERE shop_id=? AND name=? LIMIT 1", (rs,rn)->rs.getLong(1), shopId, name);
if (!ids.isEmpty()) return ids.get(0);
jdbcTemplate.update("INSERT INTO suppliers (shop_id,user_id,name,status,created_at,updated_at) VALUES (?,?,?,1,NOW(),NOW())",
shopId, userId, name);
ids = jdbcTemplate.query("SELECT id FROM suppliers WHERE shop_id=? AND name=? LIMIT 1", (rs,rn)->rs.getLong(1), shopId, name);
if (!ids.isEmpty()) return ids.get(0);
throw new IllegalStateException("默认供应商创建失败");
}
private BigDecimal resolveProductCostPrice(Long productId, Long shopId) { private BigDecimal resolveProductCostPrice(Long productId, Long shopId) {
return productPriceRepository.findById(productId) return productPriceRepository.findById(productId)
.filter(price -> price.getShopId().equals(shopId)) .filter(price -> price.getShopId().equals(shopId))

View File

@@ -49,11 +49,14 @@ public class MetadataController {
@GetMapping("/api/product-templates") @GetMapping("/api/product-templates")
public ResponseEntity<?> listTemplates(@RequestParam(name = "categoryId", required = false) Long categoryId) { public ResponseEntity<?> listTemplates(@RequestParam(name = "categoryId", required = false) Long categoryId) {
Map<String, Object> body = new HashMap<>(); Map<String, Object> body = new HashMap<>();
// 排除已软删模板;仍要求 status=1 才可见
java.util.List<com.example.demo.product.entity.PartTemplate> list = java.util.List<com.example.demo.product.entity.PartTemplate> list =
categoryId == null ? templateRepository.findByStatusOrderByIdDesc(1) (categoryId == null)
: templateRepository.findByStatusAndCategoryIdOrderByIdDesc(1, categoryId); ? templateRepository.findByStatusOrderByIdDesc(1)
: templateRepository.findByStatusAndCategoryIdOrderByIdDesc(1, categoryId);
java.util.List<java.util.Map<String,Object>> out = new java.util.ArrayList<>(); java.util.List<java.util.Map<String,Object>> out = new java.util.ArrayList<>();
for (com.example.demo.product.entity.PartTemplate t : list) { for (com.example.demo.product.entity.PartTemplate t : list) {
try { if (t.getDeletedAt() != null) continue; } catch (Exception ignore) {}
java.util.Map<String,Object> m = new java.util.HashMap<>(); java.util.Map<String,Object> m = new java.util.HashMap<>();
m.put("id", t.getId()); m.put("id", t.getId());
m.put("categoryId", t.getCategoryId()); m.put("categoryId", t.getCategoryId());

View File

@@ -1,7 +1,9 @@
package com.example.demo.product.controller; package com.example.demo.product.controller;
import com.example.demo.product.service.ProductSubmissionService; import com.example.demo.product.service.ProductSubmissionService;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
@RestController @RestController
@@ -9,19 +11,28 @@ import org.springframework.web.bind.annotation.*;
public class NormalAdminSubmissionController { public class NormalAdminSubmissionController {
private final ProductSubmissionService submissionService; private final ProductSubmissionService submissionService;
private final JdbcTemplate jdbc;
public NormalAdminSubmissionController(ProductSubmissionService submissionService) { public NormalAdminSubmissionController(ProductSubmissionService submissionService,
JdbcTemplate jdbc) {
this.submissionService = submissionService; this.submissionService = submissionService;
this.jdbc = jdbc;
} }
// 代理现有管理端接口,但不暴露跨店查询参数,实际范围由拦截器限定 private Long findShopIdByUser(Long userId) {
if (userId == null) return null;
return jdbc.query("SELECT shop_id FROM users WHERE id=?", ps -> ps.setLong(1, userId), rs -> rs.next()? rs.getLong(1): null);
}
// 代理现有管理端接口,但范围限定为当前用户所属店铺
@GetMapping("/submissions") @GetMapping("/submissions")
public ResponseEntity<?> list(@RequestParam(name = "status", required = false) String status, public ResponseEntity<?> list(@RequestHeader(name = "X-User-Id", required = false) Long userId,
@RequestParam(name = "status", required = false) String status,
@RequestParam(name = "kw", required = false) String kw, @RequestParam(name = "kw", required = false) String kw,
@RequestParam(name = "page", defaultValue = "1") int page, @RequestParam(name = "page", defaultValue = "1") int page,
@RequestParam(name = "size", defaultValue = "20") int size) { @RequestParam(name = "size", defaultValue = "20") int size) {
// 普通管理端不允许跨店过滤reviewer/shopId 均不提供 Long shopId = findShopIdByUser(userId);
return ResponseEntity.ok(submissionService.listAdmin(status, kw, null, null, null, null, page, size)); return ResponseEntity.ok(submissionService.listAdmin(status, kw, shopId, null, null, null, page, size));
} }
@GetMapping("/submissions/{id}") @GetMapping("/submissions/{id}")
@@ -42,7 +53,6 @@ public class NormalAdminSubmissionController {
public ResponseEntity<?> approve(@PathVariable("id") Long id, public ResponseEntity<?> approve(@PathVariable("id") Long id,
@RequestHeader(name = "X-User-Id", required = false) Long userId, @RequestHeader(name = "X-User-Id", required = false) Long userId,
@RequestBody(required = false) com.example.demo.product.dto.ProductSubmissionDtos.ApproveRequest req) { @RequestBody(required = false) com.example.demo.product.dto.ProductSubmissionDtos.ApproveRequest req) {
// 这里将 X-User-Id 作为审批人记录(普通管理员为用户表)
var resp = submissionService.approve(id, userId, req); var resp = submissionService.approve(id, userId, req);
return ResponseEntity.ok(resp); return ResponseEntity.ok(resp);
} }
@@ -54,6 +64,15 @@ public class NormalAdminSubmissionController {
submissionService.reject(id, userId, req); submissionService.reject(id, userId, req);
return ResponseEntity.ok(java.util.Map.of("ok", true)); return ResponseEntity.ok(java.util.Map.of("ok", true));
} }
@GetMapping("/submissions/export")
public void export(@RequestHeader(name = "X-User-Id", required = false) Long userId,
@RequestParam(name = "status", required = false) String status,
@RequestParam(name = "kw", required = false) String kw,
HttpServletResponse response) {
Long shopId = findShopIdByUser(userId);
submissionService.export(status, kw, shopId, null, null, null, response);
}
} }

View File

@@ -38,6 +38,15 @@ public class PartTemplateController {
public ResponseEntity<?> list() { public ResponseEntity<?> list() {
return ResponseEntity.ok(templateService.list()); return ResponseEntity.ok(templateService.list());
} }
@DeleteMapping("/{id}")
public ResponseEntity<?> delete(@PathVariable("id") Long id,
@RequestParam(value = "force", required = false) Boolean force) {
templateService.delete(id, Boolean.TRUE.equals(force));
return ResponseEntity.ok(java.util.Map.of("ok", true));
}
// 分类级联软删将在 AdminDictController 中触发;此处保持模板单体删除逻辑
} }

View File

@@ -23,18 +23,29 @@ public class ProductController {
public ResponseEntity<?> search(@RequestHeader(name = "X-Shop-Id", required = false) Long shopId, public ResponseEntity<?> search(@RequestHeader(name = "X-Shop-Id", required = false) Long shopId,
@RequestParam(name = "kw", required = false) String kw, @RequestParam(name = "kw", required = false) String kw,
@RequestParam(name = "categoryId", required = false) Long categoryId, @RequestParam(name = "categoryId", required = false) Long categoryId,
@RequestParam(name = "templateId", required = false) Long templateId,
@RequestParam java.util.Map<String, String> requestParams,
@RequestParam(name = "page", defaultValue = "1") int page, @RequestParam(name = "page", defaultValue = "1") int page,
@RequestParam(name = "size", defaultValue = "50") int size) { @RequestParam(name = "size", defaultValue = "50") int size) {
Long sid = (shopId == null ? defaults.getShopId() : shopId); Long sid = (shopId == null ? defaults.getShopId() : shopId);
Page<ProductDtos.ProductListItem> result = productService.search(sid, kw, categoryId, Math.max(page - 1, 0), size); java.util.Map<String, String> paramFilters = new java.util.HashMap<>();
for (java.util.Map.Entry<String, String> e : requestParams.entrySet()) {
String k = e.getKey();
if (k != null && k.startsWith("param_") && e.getValue() != null && !e.getValue().isBlank()) {
String key = k.substring(6);
if (!key.isBlank()) paramFilters.put(key, e.getValue());
}
}
Page<ProductDtos.ProductListItem> result = productService.search(sid, kw, categoryId, templateId, paramFilters, Math.max(page - 1, 0), size);
java.util.Map<String, Object> body = new java.util.HashMap<>(); java.util.Map<String, Object> body = new java.util.HashMap<>();
body.put("list", result.getContent()); body.put("list", result.getContent());
return ResponseEntity.ok(body); return ResponseEntity.ok(body);
} }
@GetMapping("/{id}") @GetMapping("/{id}")
public ResponseEntity<?> detail(@PathVariable("id") Long id) { public ResponseEntity<?> detail(@PathVariable("id") Long id,
return productService.findDetail(id) @RequestParam(name = "includeDeleted", required = false, defaultValue = "false") boolean includeDeleted) {
return productService.findDetail(id, includeDeleted)
.<ResponseEntity<?>>map(ResponseEntity::ok) .<ResponseEntity<?>>map(ResponseEntity::ok)
.orElseGet(() -> ResponseEntity.notFound().build()); .orElseGet(() -> ResponseEntity.notFound().build());
} }
@@ -61,6 +72,15 @@ public class ProductController {
productService.update(id, sid, uid, req); productService.update(id, sid, uid, req);
return ResponseEntity.ok().build(); return ResponseEntity.ok().build();
} }
@DeleteMapping("/{id}")
public ResponseEntity<?> delete(@PathVariable("id") Long id,
@RequestHeader(name = "X-Shop-Id", required = false) Long shopId,
@RequestHeader(name = "X-User-Id", required = false) Long userId) {
Long sid = (shopId == null ? defaults.getShopId() : shopId);
productService.delete(id, sid);
return ResponseEntity.ok().build();
}
} }

View File

@@ -15,6 +15,7 @@ public class ProductDtos {
public BigDecimal stock; // from inventories.quantity public BigDecimal stock; // from inventories.quantity
public BigDecimal retailPrice; // from product_prices public BigDecimal retailPrice; // from product_prices
public String cover; // first image url public String cover; // first image url
public Boolean deleted; // derived from deleted_at
} }
public static class ProductDetail { public static class ProductDetail {
@@ -26,7 +27,8 @@ public class ProductDtos {
public String spec; public String spec;
public String origin; public String origin;
public Long categoryId; public Long categoryId;
public Long unitId; // 单位字段已移除
public Long templateId;
public BigDecimal safeMin; public BigDecimal safeMin;
public BigDecimal safeMax; public BigDecimal safeMax;
public BigDecimal stock; public BigDecimal stock;
@@ -35,6 +37,10 @@ public class ProductDtos {
public BigDecimal wholesalePrice; public BigDecimal wholesalePrice;
public BigDecimal bigClientPrice; public BigDecimal bigClientPrice;
public List<Image> images; public List<Image> images;
public Map<String, Object> parameters;
public Long sourceSubmissionId;
public String externalCode;
public Boolean deleted;
} }
public static class Image { public static class Image {

View File

@@ -8,12 +8,13 @@ public class ProductSubmissionDtos {
public static class CreateRequest { public static class CreateRequest {
public Long templateId; public Long templateId;
public String externalCode; // 外部编号
public String name; public String name;
public String model; public String model;
public String brand; public String brand;
public String spec; public String spec;
public String origin; public String origin;
public Long unitId; // 单位字段已移除
public Long categoryId; public Long categoryId;
public Map<String, Object> parameters; public Map<String, Object> parameters;
public List<String> images; public List<String> images;
@@ -25,11 +26,12 @@ public class ProductSubmissionDtos {
public static class UpdateRequest { public static class UpdateRequest {
public Long templateId; public Long templateId;
public String externalCode; // 外部编号
public String name; public String name;
public String brand; public String brand;
public String spec; public String spec;
public String origin; public String origin;
public Long unitId; // 单位字段已移除
public Long categoryId; public Long categoryId;
public Map<String, Object> parameters; public Map<String, Object> parameters;
public List<String> images; public List<String> images;
@@ -72,12 +74,13 @@ public class ProductSubmissionDtos {
public Long shopId; public Long shopId;
public Long userId; public Long userId;
public Long templateId; public Long templateId;
public String externalCode;
public String name; public String name;
public String model; public String model;
public String brand; public String brand;
public String spec; public String spec;
public String origin; public String origin;
public Long unitId; // 单位字段已移除
public Long categoryId; public Long categoryId;
public Map<String, Object> parameters; public Map<String, Object> parameters;
public List<String> images; public List<String> images;

View File

@@ -32,6 +32,9 @@ public class PartTemplate {
@Column(name = "updated_at") @Column(name = "updated_at")
private LocalDateTime updatedAt; private LocalDateTime updatedAt;
@Column(name = "deleted_at")
private LocalDateTime deletedAt;
public Long getId() { return id; } public Long getId() { return id; }
public Long getCategoryId() { return categoryId; } public Long getCategoryId() { return categoryId; }
public void setCategoryId(Long categoryId) { this.categoryId = categoryId; } public void setCategoryId(Long categoryId) { this.categoryId = categoryId; }
@@ -47,6 +50,8 @@ public class PartTemplate {
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; } public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
public LocalDateTime getUpdatedAt() { return updatedAt; } public LocalDateTime getUpdatedAt() { return updatedAt; }
public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; } public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
public LocalDateTime getDeletedAt() { return deletedAt; }
public void setDeletedAt(LocalDateTime deletedAt) { this.deletedAt = deletedAt; }
} }

View File

@@ -24,9 +24,6 @@ public class Product {
@Column(name = "category_id") @Column(name = "category_id")
private Long categoryId; private Long categoryId;
@Column(name = "unit_id", nullable = false)
private Long unitId;
@Column(name = "template_id") @Column(name = "template_id")
private Long templateId; private Long templateId;
@@ -84,8 +81,6 @@ public class Product {
public void setName(String name) { this.name = name; } public void setName(String name) { this.name = name; }
public Long getCategoryId() { return categoryId; } public Long getCategoryId() { return categoryId; }
public void setCategoryId(Long categoryId) { this.categoryId = categoryId; } public void setCategoryId(Long categoryId) { this.categoryId = categoryId; }
public Long getUnitId() { return unitId; }
public void setUnitId(Long unitId) { this.unitId = unitId; }
public Long getTemplateId() { return templateId; } public Long getTemplateId() { return templateId; }
public void setTemplateId(Long templateId) { this.templateId = templateId; } public void setTemplateId(Long templateId) { this.templateId = templateId; }
public String getBrand() { return brand; } public String getBrand() { return brand; }

View File

@@ -6,6 +6,7 @@ import org.springframework.data.jpa.repository.JpaRepository;
public interface PartTemplateRepository extends JpaRepository<PartTemplate, Long> { public interface PartTemplateRepository extends JpaRepository<PartTemplate, Long> {
java.util.List<PartTemplate> findByStatusOrderByIdDesc(Integer status); java.util.List<PartTemplate> findByStatusOrderByIdDesc(Integer status);
java.util.List<PartTemplate> findByStatusAndCategoryIdOrderByIdDesc(Integer status, Long categoryId); java.util.List<PartTemplate> findByStatusAndCategoryIdOrderByIdDesc(Integer status, Long categoryId);
java.util.List<PartTemplate> findByDeletedAtIsNullOrderByIdDesc();
} }

View File

@@ -11,10 +11,11 @@ public interface ProductRepository extends JpaRepository<Product, Long> {
@Query("SELECT p FROM Product p WHERE p.shopId = :shopId AND (p.deletedAt IS NULL) AND " + @Query("SELECT p FROM Product p WHERE p.shopId = :shopId AND (p.deletedAt IS NULL) AND " +
"(:kw IS NULL OR :kw = '' OR p.name LIKE CONCAT('%', :kw, '%') OR p.brand LIKE CONCAT('%', :kw, '%') OR p.model LIKE CONCAT('%', :kw, '%') OR p.spec LIKE CONCAT('%', :kw, '%') OR p.barcode LIKE CONCAT('%', :kw, '%')) AND " + "(:kw IS NULL OR :kw = '' OR p.name LIKE CONCAT('%', :kw, '%') OR p.brand LIKE CONCAT('%', :kw, '%') OR p.model LIKE CONCAT('%', :kw, '%') OR p.spec LIKE CONCAT('%', :kw, '%') OR p.barcode LIKE CONCAT('%', :kw, '%')) AND " +
"(:categoryId IS NULL OR p.categoryId = :categoryId) ORDER BY p.id DESC") "(:categoryId IS NULL OR p.categoryId = :categoryId) AND (:templateId IS NULL OR p.templateId = :templateId) ORDER BY p.id DESC")
Page<Product> search(@Param("shopId") Long shopId, Page<Product> search(@Param("shopId") Long shopId,
@Param("kw") String kw, @Param("kw") String kw,
@Param("categoryId") Long categoryId, @Param("categoryId") Long categoryId,
@Param("templateId") Long templateId,
Pageable pageable); Pageable pageable);
boolean existsByShopIdAndBarcode(Long shopId, String barcode); boolean existsByShopIdAndBarcode(Long shopId, String barcode);

View File

@@ -14,7 +14,9 @@ public interface ProductSubmissionRepository extends JpaRepository<ProductSubmis
Page<ProductSubmission> findByShopIdAndUserIdAndStatusIn(Long shopId, Long userId, List<ProductSubmission.Status> statuses, Pageable pageable); Page<ProductSubmission> findByShopIdAndUserIdAndStatusIn(Long shopId, Long userId, List<ProductSubmission.Status> statuses, Pageable pageable);
@Query("SELECT ps FROM ProductSubmission ps WHERE (:statusList IS NULL OR ps.status IN :statusList) " + Page<ProductSubmission> findByShopIdAndUserIdAndStatusInAndDeletedAtIsNull(Long shopId, Long userId, List<ProductSubmission.Status> statuses, Pageable pageable);
@Query("SELECT ps FROM ProductSubmission ps WHERE ps.deletedAt IS NULL AND (:statusList IS NULL OR ps.status IN :statusList) " +
"AND (:kw IS NULL OR ps.modelUnique LIKE :kw OR ps.name LIKE :kw OR ps.brand LIKE :kw) " + "AND (:kw IS NULL OR ps.modelUnique LIKE :kw OR ps.name LIKE :kw OR ps.brand LIKE :kw) " +
"AND (:shopId IS NULL OR ps.shopId = :shopId) " + "AND (:shopId IS NULL OR ps.shopId = :shopId) " +
"AND (:reviewerId IS NULL OR ps.reviewerId = :reviewerId) " + "AND (:reviewerId IS NULL OR ps.reviewerId = :reviewerId) " +

View File

@@ -64,6 +64,15 @@ public class PartTemplateService {
if (req.modelRule != null) t.setModelRule(req.modelRule); if (req.modelRule != null) t.setModelRule(req.modelRule);
if (req.status != null) t.setStatus(req.status); if (req.status != null) t.setStatus(req.status);
t.setUpdatedAt(LocalDateTime.now()); t.setUpdatedAt(LocalDateTime.now());
try {
// 若模板已被软删,不允许通过 update 将其“启用”,需运维恢复
java.lang.reflect.Field f = t.getClass().getDeclaredField("deletedAt");
f.setAccessible(true);
Object v = f.get(t);
if (v != null && (req.status != null && req.status == 1)) {
throw new IllegalStateException("模板已删除,无法启用。请联系平台管理员");
}
} catch (NoSuchFieldException ignore) { } catch (IllegalAccessException ignore) { }
templateRepository.save(t); templateRepository.save(t);
if (req.params != null) { if (req.params != null) {
@@ -117,6 +126,32 @@ public class PartTemplateService {
return out; return out;
} }
@Transactional
public void delete(Long id, boolean force) {
if (!force) {
// 软删除:隐藏模板并级联软删该模板下商品
PartTemplate t = templateRepository.findById(id).orElseThrow();
t.setStatus(0);
t.setUpdatedAt(LocalDateTime.now());
// 统一软删标记:写入 deleted_at
try { jdbcTemplate.update("UPDATE part_templates SET deleted_at=NOW() WHERE id=? AND deleted_at IS NULL", id); } catch (Exception ignore) {}
templateRepository.save(t);
// 级联软删商品与配件提交
jdbcTemplate.update("UPDATE products SET deleted_at=NOW() WHERE template_id=? AND deleted_at IS NULL", id);
jdbcTemplate.update("UPDATE part_submissions SET deleted_at=NOW() WHERE template_id=? AND deleted_at IS NULL", id);
return;
}
// 永久删除:删除参数与模板,并清理关联数据
paramRepository.findByTemplateIdOrderBySortOrderAscIdAsc(id).forEach(p -> paramRepository.deleteById(p.getId()));
jdbcTemplate.update("UPDATE products SET deleted_at=NOW() WHERE template_id=? AND deleted_at IS NULL", id);
jdbcTemplate.update("DELETE FROM product_images WHERE product_id IN (SELECT id FROM products WHERE template_id=? )", id);
jdbcTemplate.update("DELETE FROM product_prices WHERE product_id IN (SELECT id FROM products WHERE template_id=? )", id);
jdbcTemplate.update("DELETE FROM inventories WHERE product_id IN (SELECT id FROM products WHERE template_id=? )", id);
jdbcTemplate.update("UPDATE part_submissions SET deleted_at=NOW() WHERE template_id=? AND deleted_at IS NULL", id);
templateRepository.deleteById(id);
}
private void upsertParams(Long templateId, List<PartTemplateDtos.ParamDef> params, LocalDateTime now) { private void upsertParams(Long templateId, List<PartTemplateDtos.ParamDef> params, LocalDateTime now) {
if (params == null) return; if (params == null) return;
int idx = 0; int idx = 0;

View File

@@ -96,10 +96,7 @@ public class ProductService {
product.setCategoryId(submission.getCategoryId()); product.setCategoryId(submission.getCategoryId());
changed = true; changed = true;
} }
if (submission.getUnitId() != null && !submission.getUnitId().equals(product.getUnitId())) { // 单位字段已移除
product.setUnitId(submission.getUnitId());
changed = true;
}
if (submission.getRemarkText() != null && !submission.getRemarkText().isBlank()) { if (submission.getRemarkText() != null && !submission.getRemarkText().isBlank()) {
product.setDescription(submission.getRemarkText()); product.setDescription(submission.getRemarkText());
changed = true; changed = true;
@@ -137,58 +134,55 @@ public class ProductService {
syncImages(submission.getUserId(), productId, product.getShopId(), images); syncImages(submission.getUserId(), productId, product.getShopId(), images);
} }
public Page<ProductDtos.ProductListItem> search(Long shopId, String kw, Long categoryId, int page, int size) { public Page<ProductDtos.ProductListItem> search(Long shopId, String kw, Long categoryId, Long templateId, java.util.Map<String,String> paramFilters, int page, int size) {
try { // 直接使用 JDBC 支持 JSON_EXTRACT 过滤MySQL
Page<Product> p = productRepository.search(shopId, kw, categoryId, PageRequest.of(page, size)); StringBuilder sql = new StringBuilder("SELECT p.id,p.name,p.brand,p.model,p.spec,\n" +
return p.map(prod -> { "(SELECT i.quantity FROM inventories i WHERE i.product_id=p.id) AS stock,\n" +
ProductDtos.ProductListItem it = new ProductDtos.ProductListItem(); "(SELECT pr.retail_price FROM product_prices pr WHERE pr.product_id=p.id) AS retail_price,\n" +
it.id = prod.getId(); "(SELECT img.url FROM product_images img WHERE img.product_id=p.id ORDER BY img.sort_order, img.id LIMIT 1) AS cover,\n" +
it.name = prod.getName(); "(p.deleted_at IS NOT NULL) AS deleted\n" +
it.brand = prod.getBrand(); "FROM products p WHERE p.shop_id=? AND p.deleted_at IS NULL");
it.model = prod.getModel(); List<Object> ps = new ArrayList<>();
it.spec = prod.getSpec(); ps.add(shopId);
inventoryRepository.findById(prod.getId()).ifPresent(inv -> it.stock = inv.getQuantity()); if (kw != null && !kw.isBlank()) { sql.append(" AND (p.name LIKE ? OR p.brand LIKE ? OR p.model LIKE ? OR p.spec LIKE ? OR p.barcode LIKE ?)");
priceRepository.findById(prod.getId()).ifPresent(pr -> it.retailPrice = pr.getRetailPrice()); String like = "%" + kw + "%"; ps.add(like); ps.add(like); ps.add(like); ps.add(like); ps.add(like); }
List<ProductImage> imgs = imageRepository.findByProductIdOrderBySortOrderAscIdAsc(prod.getId()); if (categoryId != null) { sql.append(" AND p.category_id=?"); ps.add(categoryId); }
it.cover = imgs.isEmpty() ? null : imgs.get(0).getUrl(); if (templateId != null) { sql.append(" AND p.template_id=?"); ps.add(templateId); }
return it; if (paramFilters != null && !paramFilters.isEmpty()) {
}); for (java.util.Map.Entry<String,String> ent : paramFilters.entrySet()) {
} catch (Exception e) { String key = ent.getKey(); String val = ent.getValue();
// 安全回退为 JDBC 查询,保障功能可用 if (key == null || key.isBlank() || val == null || val.isBlank()) continue;
StringBuilder sql = new StringBuilder("SELECT p.id,p.name,p.brand,p.model,p.spec,\n" + // 精确匹配参数值:将 JSON 值解包后与入参做等值比较,避免 LIKE 导致的误匹配
"(SELECT i.quantity FROM inventories i WHERE i.product_id=p.id) AS stock,\n" + sql.append(" AND JSON_UNQUOTE(JSON_EXTRACT(p.attributes_json, ?)) = ?");
"(SELECT pr.retail_price FROM product_prices pr WHERE pr.product_id=p.id) AS retail_price,\n" + ps.add("$." + key);
"(SELECT img.url FROM product_images img WHERE img.product_id=p.id ORDER BY img.sort_order, img.id LIMIT 1) AS cover\n" + ps.add(val.trim());
"FROM products p WHERE p.shop_id=? AND p.deleted_at IS NULL"); }
List<Object> ps = new ArrayList<>();
ps.add(shopId);
if (kw != null && !kw.isBlank()) { sql.append(" AND (p.name LIKE ? OR p.brand LIKE ? OR p.model LIKE ? OR p.spec LIKE ? OR p.barcode LIKE ?)");
String like = "%" + kw + "%"; ps.add(like); ps.add(like); ps.add(like); ps.add(like); ps.add(like); }
if (categoryId != null) { sql.append(" AND p.category_id=?"); ps.add(categoryId); }
sql.append(" ORDER BY p.id DESC LIMIT ? OFFSET ?");
ps.add(size); ps.add(page * size);
List<ProductDtos.ProductListItem> list = jdbcTemplate.query(sql.toString(), (rs,rn) -> {
ProductDtos.ProductListItem it = new ProductDtos.ProductListItem();
it.id = rs.getLong("id");
it.name = rs.getString("name");
it.brand = rs.getString("brand");
it.model = rs.getString("model");
it.spec = rs.getString("spec");
java.math.BigDecimal st = (java.math.BigDecimal) rs.getObject("stock");
it.stock = st;
java.math.BigDecimal rp = (java.math.BigDecimal) rs.getObject("retail_price");
it.retailPrice = rp;
it.cover = rs.getString("cover");
return it;
}, ps.toArray());
return new PageImpl<>(list, PageRequest.of(page, size), list.size());
} }
sql.append(" ORDER BY p.id DESC LIMIT ? OFFSET ?");
ps.add(size); ps.add(page * size);
List<ProductDtos.ProductListItem> list = jdbcTemplate.query(sql.toString(), (rs,rn) -> {
ProductDtos.ProductListItem it = new ProductDtos.ProductListItem();
it.id = rs.getLong("id");
it.name = rs.getString("name");
it.brand = rs.getString("brand");
it.model = rs.getString("model");
it.spec = rs.getString("spec");
java.math.BigDecimal st = (java.math.BigDecimal) rs.getObject("stock");
it.stock = st;
java.math.BigDecimal rp = (java.math.BigDecimal) rs.getObject("retail_price");
it.retailPrice = rp;
it.cover = rs.getString("cover");
it.deleted = rs.getBoolean("deleted");
return it;
}, ps.toArray());
return new PageImpl<>(list, PageRequest.of(page, size), list.size());
} }
public Optional<ProductDtos.ProductDetail> findDetail(Long id) { public Optional<ProductDtos.ProductDetail> findDetail(Long id, boolean includeDeleted) {
Optional<Product> op = productRepository.findById(id); Optional<Product> op = productRepository.findById(id);
if (op.isEmpty()) return Optional.empty(); if (op.isEmpty()) return Optional.empty();
Product p = op.get(); Product p = op.get();
if (p.getDeletedAt() != null && !includeDeleted) return Optional.empty();
ProductDtos.ProductDetail d = new ProductDtos.ProductDetail(); ProductDtos.ProductDetail d = new ProductDtos.ProductDetail();
d.id = p.getId(); d.id = p.getId();
d.name = p.getName(); d.name = p.getName();
@@ -198,7 +192,7 @@ public class ProductService {
d.spec = p.getSpec(); d.spec = p.getSpec();
d.origin = p.getOrigin(); d.origin = p.getOrigin();
d.categoryId = p.getCategoryId(); d.categoryId = p.getCategoryId();
d.unitId = p.getUnitId(); d.templateId = p.getTemplateId();
d.safeMin = p.getSafeMin(); d.safeMin = p.getSafeMin();
d.safeMax = p.getSafeMax(); d.safeMax = p.getSafeMax();
inventoryRepository.findById(p.getId()).ifPresent(inv -> d.stock = inv.getQuantity()); inventoryRepository.findById(p.getId()).ifPresent(inv -> d.stock = inv.getQuantity());
@@ -216,6 +210,11 @@ public class ProductService {
list.add(i); list.add(i);
} }
d.images = list; d.images = list;
d.parameters = JsonUtils.fromJson(p.getAttributesJson(), new com.fasterxml.jackson.core.type.TypeReference<java.util.Map<String,Object>>() {});
d.sourceSubmissionId = p.getSourceSubmissionId();
// deleted 标志供前端展示
d.deleted = (p.getDeletedAt() != null);
// externalCode 来自 submission若来源存在可透传此处留空由前端兼容
return Optional.of(d); return Optional.of(d);
} }
@@ -235,7 +234,7 @@ public class ProductService {
p.setSpec(emptyToNull(req.spec)); p.setSpec(emptyToNull(req.spec));
p.setOrigin(emptyToNull(req.origin)); p.setOrigin(emptyToNull(req.origin));
p.setCategoryId(req.categoryId); p.setCategoryId(req.categoryId);
p.setUnitId(req.unitId); // 单位字段已移除
p.setDedupeKey(emptyToNull(req.dedupeKey)); p.setDedupeKey(emptyToNull(req.dedupeKey));
p.setSafeMin(req.safeMin); p.setSafeMin(req.safeMin);
p.setSafeMax(req.safeMax); p.setSafeMax(req.safeMax);
@@ -290,7 +289,7 @@ public class ProductService {
p.setSpec(emptyToNull(req.spec)); p.setSpec(emptyToNull(req.spec));
p.setOrigin(emptyToNull(req.origin)); p.setOrigin(emptyToNull(req.origin));
p.setCategoryId(req.categoryId); p.setCategoryId(req.categoryId);
p.setUnitId(req.unitId); // 单位字段已移除
p.setDedupeKey(emptyToNull(req.dedupeKey)); p.setDedupeKey(emptyToNull(req.dedupeKey));
p.setSafeMin(req.safeMin); p.setSafeMin(req.safeMin);
p.setSafeMax(req.safeMax); p.setSafeMax(req.safeMax);
@@ -306,7 +305,7 @@ public class ProductService {
private void validate(Long shopId, ProductDtos.CreateOrUpdateProductRequest req) { private void validate(Long shopId, ProductDtos.CreateOrUpdateProductRequest req) {
if (req.name == null || req.name.isBlank()) throw new IllegalArgumentException("name必填"); if (req.name == null || req.name.isBlank()) throw new IllegalArgumentException("name必填");
if (req.unitId == null) throw new IllegalArgumentException("unitId必填"); // 不再要求 unitId
if (req.safeMin != null && req.safeMax != null) { if (req.safeMin != null && req.safeMax != null) {
if (req.safeMin.compareTo(req.safeMax) > 0) throw new IllegalArgumentException("安全库存区间不合法"); if (req.safeMin.compareTo(req.safeMax) > 0) throw new IllegalArgumentException("安全库存区间不合法");
} }
@@ -360,6 +359,16 @@ public class ProductService {
private static <T> T nvl(T v, T def) { return v != null ? v : def; } private static <T> T nvl(T v, T def) { return v != null ? v : def; }
private static String emptyToNull(String s) { return (s == null || s.isBlank()) ? null : s; } private static String emptyToNull(String s) { return (s == null || s.isBlank()) ? null : s; }
@Transactional
public void delete(Long id, Long shopId) {
Product p = productRepository.findById(id).orElse(null);
if (p == null) return;
if (!p.getShopId().equals(shopId)) throw new IllegalArgumentException("跨店铺数据");
p.setDeletedAt(LocalDateTime.now());
productRepository.save(p);
// 关联数据:价格/库存采用 ON DELETE CASCADE 不触发;软删仅标记主表
}
} }

View File

@@ -14,6 +14,7 @@ import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@@ -43,15 +44,18 @@ public class ProductSubmissionService {
private final ProductService productService; private final ProductService productService;
private final PartTemplateParamRepository templateParamRepository; private final PartTemplateParamRepository templateParamRepository;
private final AppDefaultsProperties defaults; private final AppDefaultsProperties defaults;
private final JdbcTemplate jdbcTemplate;
public ProductSubmissionService(ProductSubmissionRepository submissionRepository, public ProductSubmissionService(ProductSubmissionRepository submissionRepository,
ProductService productService, ProductService productService,
AppDefaultsProperties defaults, AppDefaultsProperties defaults,
PartTemplateParamRepository templateParamRepository) { PartTemplateParamRepository templateParamRepository,
JdbcTemplate jdbcTemplate) {
this.submissionRepository = submissionRepository; this.submissionRepository = submissionRepository;
this.productService = productService; this.productService = productService;
this.defaults = defaults; this.defaults = defaults;
this.templateParamRepository = templateParamRepository; this.templateParamRepository = templateParamRepository;
this.jdbcTemplate = jdbcTemplate;
} }
@Transactional @Transactional
@@ -61,12 +65,13 @@ public class ProductSubmissionService {
submission.setShopId(shopId); submission.setShopId(shopId);
submission.setUserId(userId); submission.setUserId(userId);
submission.setTemplateId(req.templateId); submission.setTemplateId(req.templateId);
if (req.externalCode != null && !req.externalCode.isBlank()) submission.setExternalCode(req.externalCode.trim());
submission.setName(req.name); submission.setName(req.name);
submission.setModelUnique(normalizeModel(req.model)); submission.setModelUnique(normalizeModel(req.model));
submission.setBrand(req.brand); submission.setBrand(req.brand);
submission.setSpec(req.spec); submission.setSpec(req.spec);
submission.setOrigin(req.origin); submission.setOrigin(req.origin);
submission.setUnitId(req.unitId); // 单位字段已移除
submission.setCategoryId(req.categoryId); submission.setCategoryId(req.categoryId);
submission.setAttributesJson(JsonUtils.toJson(req.parameters)); submission.setAttributesJson(JsonUtils.toJson(req.parameters));
submission.setImagesJson(JsonUtils.toJson(req.images)); submission.setImagesJson(JsonUtils.toJson(req.images));
@@ -84,7 +89,7 @@ public class ProductSubmissionService {
public ProductSubmissionDtos.PageResult<ProductSubmissionDtos.SubmissionItem> listMine(Long shopId, Long userId, String status, int page, int size) { public ProductSubmissionDtos.PageResult<ProductSubmissionDtos.SubmissionItem> listMine(Long shopId, Long userId, String status, int page, int size) {
String normalizedStatus = (status == null || status.isBlank() || "undefined".equalsIgnoreCase(status)) ? null : status; String normalizedStatus = (status == null || status.isBlank() || "undefined".equalsIgnoreCase(status)) ? null : status;
Page<ProductSubmission> result = submissionRepository.findByShopIdAndUserIdAndStatusIn( Page<ProductSubmission> result = submissionRepository.findByShopIdAndUserIdAndStatusInAndDeletedAtIsNull(
shopId, userId, resolveStatuses(normalizedStatus), PageRequest.of(Math.max(page - 1, 0), size, Sort.by(Sort.Direction.DESC, "createdAt"))); shopId, userId, resolveStatuses(normalizedStatus), PageRequest.of(Math.max(page - 1, 0), size, Sort.by(Sort.Direction.DESC, "createdAt")));
return toPageResult(result); return toPageResult(result);
} }
@@ -109,6 +114,26 @@ public class ProductSubmissionService {
LocalDateTime endTime = parseDate(endAt); LocalDateTime endTime = parseDate(endAt);
List<ProductSubmission> records = submissionRepository.searchAdmin(statuses.isEmpty() ? null : statuses, List<ProductSubmission> records = submissionRepository.searchAdmin(statuses.isEmpty() ? null : statuses,
kwLike, shopId, reviewerId, start, endTime, PageRequest.of(0, 2000, Sort.by(Sort.Direction.DESC, "createdAt"))).getContent(); kwLike, shopId, reviewerId, start, endTime, PageRequest.of(0, 2000, Sort.by(Sort.Direction.DESC, "createdAt"))).getContent();
// 收集所有模板的必填参数标题
java.util.LinkedHashSet<String> requiredParamLabels = new java.util.LinkedHashSet<>();
java.util.Map<Long, java.util.Map<String,String>> labelToKeyByTemplate = new java.util.HashMap<>();
for (ProductSubmission s : records) {
Long tid = s.getTemplateId();
if (tid == null || tid <= 0) continue;
if (!labelToKeyByTemplate.containsKey(tid)) {
java.util.Map<String,String> map = new java.util.LinkedHashMap<>();
var defs = templateParamRepository.findByTemplateIdOrderBySortOrderAscIdAsc(tid);
for (var d : defs) {
if (Boolean.TRUE.equals(d.getRequired())) {
map.put(d.getFieldLabel(), d.getFieldKey());
requiredParamLabels.add(d.getFieldLabel());
}
}
labelToKeyByTemplate.put(tid, map);
} else {
for (var e : labelToKeyByTemplate.get(tid).keySet()) requiredParamLabels.add(e);
}
}
try (Workbook workbook = new XSSFWorkbook()) { try (Workbook workbook = new XSSFWorkbook()) {
Sheet sheet = workbook.createSheet("Submissions"); Sheet sheet = workbook.createSheet("Submissions");
CreationHelper creationHelper = workbook.getCreationHelper(); CreationHelper creationHelper = workbook.getCreationHelper();
@@ -117,27 +142,36 @@ public class ProductSubmissionService {
int rowIdx = 0; int rowIdx = 0;
Row header = sheet.createRow(rowIdx++); Row header = sheet.createRow(rowIdx++);
String[] headers = {"ID", "型号", "名称", "品牌", "状态", "提交人", "店铺", "提交时间", "审核时间", "审核备注"}; java.util.List<String> headers = new java.util.ArrayList<>();
for (int i = 0; i < headers.length; i++) { headers.add("编号");
header.createCell(i).setCellValue(headers[i]); headers.add("分类");
} headers.add("品牌");
headers.add("型号");
headers.addAll(requiredParamLabels);
headers.add("备注");
for (int i = 0; i < headers.size(); i++) header.createCell(i).setCellValue(headers.get(i));
for (ProductSubmission submission : records) { for (ProductSubmission submission : records) {
Row row = sheet.createRow(rowIdx++); Row row = sheet.createRow(rowIdx++);
int col = 0; int col = 0;
row.createCell(col++).setCellValue(submission.getId()); // 编号、分类、品牌、型号
row.createCell(col++).setCellValue(nvl(submission.getModelUnique())); row.createCell(col++).setCellValue(nvl(submission.getExternalCode()));
row.createCell(col++).setCellValue(nvl(submission.getName())); row.createCell(col++).setCellValue(nvl(resolveCategoryName(submission.getCategoryId())));
row.createCell(col++).setCellValue(nvl(submission.getBrand())); row.createCell(col++).setCellValue(nvl(submission.getBrand()));
row.createCell(col++).setCellValue(submission.getStatus().name()); row.createCell(col++).setCellValue(nvl(submission.getModelUnique()));
row.createCell(col++).setCellValue(submission.getUserId() != null ? submission.getUserId() : 0); // 模板必填参数值
row.createCell(col++).setCellValue(submission.getShopId() != null ? submission.getShopId() : 0); java.util.Map<String,Object> params = JsonUtils.fromJson(submission.getAttributesJson(), new com.fasterxml.jackson.core.type.TypeReference<java.util.Map<String,Object>>() {});
setDateCell(row.createCell(col++), submission.getCreatedAt(), dateStyle); java.util.Map<String,String> l2k = labelToKeyByTemplate.getOrDefault(submission.getTemplateId(), java.util.Collections.emptyMap());
setDateCell(row.createCell(col++), submission.getReviewedAt(), dateStyle); for (String label : requiredParamLabels) {
row.createCell(col).setCellValue(nvl(submission.getReviewRemark())); String key = l2k.get(label);
Object v = (key == null || params == null) ? null : params.get(key);
row.createCell(col++).setCellValue(v == null ? "" : String.valueOf(v));
}
// 备注
row.createCell(col).setCellValue(nvl(submission.getRemarkText()));
} }
for (int i = 0; i < headers.length; i++) { for (int i = 0; i < headers.size(); i++) {
sheet.autoSizeColumn(i); sheet.autoSizeColumn(i);
int width = sheet.getColumnWidth(i); int width = sheet.getColumnWidth(i);
sheet.setColumnWidth(i, Math.min(width + 512, 10000)); sheet.setColumnWidth(i, Math.min(width + 512, 10000));
@@ -153,6 +187,15 @@ public class ProductSubmissionService {
} }
} }
private final java.util.Map<Long, String> categoryNameCache = new java.util.HashMap<>();
private String resolveCategoryName(Long categoryId) {
if (categoryId == null) return "";
if (categoryNameCache.containsKey(categoryId)) return categoryNameCache.get(categoryId);
String name = jdbcTemplate.query("SELECT name FROM product_categories WHERE id=?", ps -> ps.setLong(1, categoryId), rs -> rs.next()? rs.getString(1): "");
categoryNameCache.put(categoryId, name == null ? "" : name);
return name == null ? "" : name;
}
public Optional<ProductSubmissionDtos.SubmissionDetail> findDetail(Long id) { public Optional<ProductSubmissionDtos.SubmissionDetail> findDetail(Long id) {
return submissionRepository.findById(id).map(this::toDetail); return submissionRepository.findById(id).map(this::toDetail);
} }
@@ -168,10 +211,11 @@ public class ProductSubmissionService {
throw new IllegalArgumentException("仅待审核记录可编辑"); throw new IllegalArgumentException("仅待审核记录可编辑");
} }
submission.setName(req.name != null ? req.name : submission.getName()); submission.setName(req.name != null ? req.name : submission.getName());
if (req.externalCode != null) submission.setExternalCode(req.externalCode);
submission.setBrand(req.brand != null ? req.brand : submission.getBrand()); submission.setBrand(req.brand != null ? req.brand : submission.getBrand());
submission.setSpec(req.spec != null ? req.spec : submission.getSpec()); submission.setSpec(req.spec != null ? req.spec : submission.getSpec());
submission.setOrigin(req.origin != null ? req.origin : submission.getOrigin()); submission.setOrigin(req.origin != null ? req.origin : submission.getOrigin());
submission.setUnitId(req.unitId != null ? req.unitId : submission.getUnitId()); // 单位字段已移除
submission.setCategoryId(req.categoryId != null ? req.categoryId : submission.getCategoryId()); submission.setCategoryId(req.categoryId != null ? req.categoryId : submission.getCategoryId());
if (req.parameters != null) submission.setAttributesJson(JsonUtils.toJson(req.parameters)); if (req.parameters != null) submission.setAttributesJson(JsonUtils.toJson(req.parameters));
if (req.images != null) submission.setImagesJson(JsonUtils.toJson(req.images)); if (req.images != null) submission.setImagesJson(JsonUtils.toJson(req.images));
@@ -372,11 +416,12 @@ public class ProductSubmissionService {
detail.shopId = submission.getShopId(); detail.shopId = submission.getShopId();
detail.userId = submission.getUserId(); detail.userId = submission.getUserId();
detail.name = submission.getName(); detail.name = submission.getName();
detail.externalCode = submission.getExternalCode();
detail.model = submission.getModelUnique(); detail.model = submission.getModelUnique();
detail.brand = submission.getBrand(); detail.brand = submission.getBrand();
detail.spec = submission.getSpec(); detail.spec = submission.getSpec();
detail.origin = submission.getOrigin(); detail.origin = submission.getOrigin();
detail.unitId = submission.getUnitId(); // 单位字段已移除
detail.categoryId = submission.getCategoryId(); detail.categoryId = submission.getCategoryId();
detail.templateId = submission.getTemplateId(); detail.templateId = submission.getTemplateId();
detail.parameters = JsonUtils.fromJson(submission.getAttributesJson(), new com.fasterxml.jackson.core.type.TypeReference<Map<String,Object>>() {}); detail.parameters = JsonUtils.fromJson(submission.getAttributesJson(), new com.fasterxml.jackson.core.type.TypeReference<Map<String,Object>>() {});
@@ -426,7 +471,7 @@ public class ProductSubmissionService {
payload.spec = submission.getSpec(); payload.spec = submission.getSpec();
payload.origin = submission.getOrigin(); payload.origin = submission.getOrigin();
payload.categoryId = submission.getCategoryId(); payload.categoryId = submission.getCategoryId();
payload.unitId = submission.getUnitId(); // 单位字段已移除
payload.dedupeKey = submission.getDedupeKey(); payload.dedupeKey = submission.getDedupeKey();
payload.safeMin = submission.getSafeMin(); payload.safeMin = submission.getSafeMin();
payload.safeMax = submission.getSafeMax(); payload.safeMax = submission.getSafeMax();

View File

@@ -94,3 +94,135 @@
2025-09-27 22:56:27.814 | INFO | __main__ | 启动 FastAPI 服务器: 127.0.0.1:8000 2025-09-27 22:56:27.814 | INFO | __main__ | 启动 FastAPI 服务器: 127.0.0.1:8000
2025-09-27 22:56:27.815 | DEBUG | asyncio | Using proactor: IocpProactor 2025-09-27 22:56:27.815 | DEBUG | asyncio | Using proactor: IocpProactor
2025-09-27 22:56:27.839 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]} 2025-09-27 22:56:27.839 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
2025-09-27 23:05:41.132 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
2025-09-27 23:05:41.133 | INFO | __main__ | 启动 FastAPI 服务器: 127.0.0.1:8000
2025-09-27 23:05:41.135 | DEBUG | asyncio | Using proactor: IocpProactor
2025-09-27 23:05:41.171 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
2025-09-27 23:47:53.925 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
2025-09-27 23:47:53.926 | INFO | __main__ | 启动 FastAPI 服务器: 127.0.0.1:8000
2025-09-27 23:47:53.928 | DEBUG | asyncio | Using proactor: IocpProactor
2025-09-27 23:47:53.957 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
2025-09-28 01:24:20.344 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
2025-09-28 01:24:20.346 | INFO | __main__ | 启动 FastAPI 服务器: 127.0.0.1:8000
2025-09-28 01:24:20.363 | DEBUG | asyncio | Using proactor: IocpProactor
2025-09-28 01:24:20.585 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
2025-09-28 22:00:52.538 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
2025-09-28 22:00:52.542 | INFO | __main__ | 启动 FastAPI 服务器: 127.0.0.1:8000
2025-09-28 22:00:52.560 | DEBUG | asyncio | Using proactor: IocpProactor
2025-09-28 22:00:52.876 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
2025-09-28 22:02:38.951 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
2025-09-28 22:02:38.954 | INFO | __main__ | 启动 FastAPI 服务器: 127.0.0.1:8000
2025-09-28 22:02:38.956 | DEBUG | asyncio | Using proactor: IocpProactor
2025-09-28 22:02:38.980 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
2025-09-28 22:29:34.466 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
2025-09-28 22:29:34.468 | INFO | __main__ | 启动 FastAPI 服务器: 127.0.0.1:8000
2025-09-28 22:29:34.476 | DEBUG | asyncio | Using proactor: IocpProactor
2025-09-28 22:29:34.670 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
2025-09-28 22:35:16.776 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
2025-09-28 22:35:16.779 | INFO | __main__ | 启动 FastAPI 服务器: 127.0.0.1:8000
2025-09-28 22:35:16.780 | DEBUG | asyncio | Using proactor: IocpProactor
2025-09-28 22:35:16.811 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
2025-09-28 22:38:31.014 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
2025-09-28 22:38:31.017 | INFO | __main__ | 启动 FastAPI 服务器: 127.0.0.1:8000
2025-09-28 22:38:31.020 | DEBUG | asyncio | Using proactor: IocpProactor
2025-09-28 22:38:31.048 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
2025-09-28 23:06:13.053 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
2025-09-28 23:06:13.055 | INFO | __main__ | 启动 FastAPI 服务器: 127.0.0.1:8000
2025-09-28 23:06:13.057 | DEBUG | asyncio | Using proactor: IocpProactor
2025-09-28 23:06:13.108 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
2025-09-28 23:15:20.745 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
2025-09-28 23:15:20.749 | INFO | __main__ | 启动 FastAPI 服务器: 127.0.0.1:8000
2025-09-28 23:15:20.753 | DEBUG | asyncio | Using proactor: IocpProactor
2025-09-28 23:15:20.798 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
2025-09-28 23:22:54.219 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
2025-09-28 23:22:54.223 | INFO | __main__ | 启动 FastAPI 服务器: 127.0.0.1:8000
2025-09-28 23:22:54.226 | DEBUG | asyncio | Using proactor: IocpProactor
2025-09-28 23:22:54.264 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
2025-09-28 23:23:45.474 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
2025-09-28 23:23:45.482 | INFO | __main__ | 启动 FastAPI 服务器: 127.0.0.1:8000
2025-09-28 23:23:45.490 | DEBUG | asyncio | Using proactor: IocpProactor
2025-09-28 23:23:45.530 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
2025-09-28 23:41:40.864 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
2025-09-28 23:41:40.869 | INFO | __main__ | 启动 FastAPI 服务器: 127.0.0.1:8000
2025-09-28 23:41:40.873 | DEBUG | asyncio | Using proactor: IocpProactor
2025-09-28 23:41:40.910 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
2025-09-28 23:50:01.655 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
2025-09-28 23:50:01.658 | INFO | __main__ | 启动 FastAPI 服务器: 127.0.0.1:8000
2025-09-28 23:50:01.661 | DEBUG | asyncio | Using proactor: IocpProactor
2025-09-28 23:50:01.696 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
2025-09-29 00:03:06.082 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
2025-09-29 00:03:06.095 | INFO | __main__ | 启动 FastAPI 服务器: 127.0.0.1:8000
2025-09-29 00:03:06.106 | DEBUG | asyncio | Using proactor: IocpProactor
2025-09-29 00:03:06.388 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
2025-09-29 11:43:33.011 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
2025-09-29 11:43:33.016 | INFO | __main__ | 启动 FastAPI 服务器: 127.0.0.1:8000
2025-09-29 11:43:33.032 | DEBUG | asyncio | Using proactor: IocpProactor
2025-09-29 11:43:33.358 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
2025-09-29 11:59:27.297 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
2025-09-29 11:59:27.298 | INFO | __main__ | 启动 FastAPI 服务器: 127.0.0.1:8000
2025-09-29 11:59:27.306 | DEBUG | asyncio | Using proactor: IocpProactor
2025-09-29 11:59:27.418 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
2025-09-29 12:21:49.423 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
2025-09-29 12:21:49.425 | INFO | __main__ | 启动 FastAPI 服务器: 127.0.0.1:8000
2025-09-29 12:21:49.427 | DEBUG | asyncio | Using proactor: IocpProactor
2025-09-29 12:21:49.456 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
2025-09-29 12:24:55.373 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
2025-09-29 12:24:55.375 | INFO | __main__ | 启动 FastAPI 服务器: 127.0.0.1:8000
2025-09-29 12:24:55.389 | DEBUG | asyncio | Using proactor: IocpProactor
2025-09-29 12:24:55.608 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
2025-09-29 12:54:19.142 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
2025-09-29 12:54:19.144 | INFO | __main__ | 启动 FastAPI 服务器: 127.0.0.1:8000
2025-09-29 12:54:19.146 | DEBUG | asyncio | Using proactor: IocpProactor
2025-09-29 12:54:19.168 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
2025-09-29 13:00:11.536 | DEBUG | python_multipart.multipart | Calling on_part_begin with no data
2025-09-29 13:00:11.536 | DEBUG | python_multipart.multipart | Calling on_header_field with data[38:57]
2025-09-29 13:00:11.536 | DEBUG | python_multipart.multipart | Calling on_header_value with data[59:125]
2025-09-29 13:00:11.536 | DEBUG | python_multipart.multipart | Calling on_header_end with no data
2025-09-29 13:00:11.536 | DEBUG | python_multipart.multipart | Calling on_header_field with data[127:139]
2025-09-29 13:00:11.538 | DEBUG | python_multipart.multipart | Calling on_header_value with data[141:151]
2025-09-29 13:00:11.538 | DEBUG | python_multipart.multipart | Calling on_header_end with no data
2025-09-29 13:00:11.538 | DEBUG | python_multipart.multipart | Calling on_header_field with data[153:167]
2025-09-29 13:00:11.538 | DEBUG | python_multipart.multipart | Calling on_header_value with data[169:174]
2025-09-29 13:00:11.538 | DEBUG | python_multipart.multipart | Calling on_header_end with no data
2025-09-29 13:00:11.538 | DEBUG | python_multipart.multipart | Calling on_headers_finished with no data
2025-09-29 13:00:11.538 | DEBUG | python_multipart.multipart | Calling on_part_data with data[178:69513]
2025-09-29 13:00:11.539 | DEBUG | python_multipart.multipart | Calling on_part_data with data[0:14631]
2025-09-29 13:00:11.539 | DEBUG | python_multipart.multipart | Calling on_part_end with no data
2025-09-29 13:00:11.539 | DEBUG | python_multipart.multipart | Calling on_end with no data
2025-09-29 13:00:11.550 | DEBUG | app.server.main | /api/barcode/scan 收到图像: shape=(1440, 1080, 3), dtype=uint8
2025-09-29 13:00:11.559 | DEBUG | pyzbar_engine | 调用 pyzbar: symbols=8, rotations=[0, 90, 180, 270], try_invert=True
2025-09-29 13:00:11.802 | DEBUG | pyzbar_engine | pyzbar 返回结果数: 1
2025-09-29 13:00:11.803 | DEBUG | EAN13Recognizer | pyzbar 返回 1 条结果
2025-09-29 13:00:11.803 | DEBUG | EAN13Recognizer | 输入尺寸=(1707, 1280, 3), 预处理后尺寸=(1707, 1280, 3)
2025-09-29 13:00:11.804 | DEBUG | pyzbar_engine | 调用 pyzbar: symbols=8, rotations=[0, 90, 180, 270], try_invert=True
2025-09-29 13:00:12.040 | DEBUG | pyzbar_engine | pyzbar 返回结果数: 1
2025-09-29 13:00:12.040 | DEBUG | EAN13Recognizer | pyzbar 识别到 1 条结果
2025-09-29 13:00:12.056 | DEBUG | EAN13Recognizer | ROI bbox=(372, 627, 654, 190)
2025-09-29 13:00:12.060 | DEBUG | EAN13Recognizer | 透视矫正后尺寸=(120, 413)
2025-09-29 13:00:12.098 | DEBUG | EAN13Recognizer | 自研 EAN13 解码失败
2025-09-29 13:00:12.099 | DEBUG | EAN13Recognizer | recognize_any 未命中 EAN13, others=1
2025-09-29 13:00:12.100 | INFO | app.server.main | /api/barcode/scan 命中非 EAN: type=CODE128, code=84455470401081732071, cost_ms=561.2
2025-09-29 13:11:28.027 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
2025-09-29 13:11:28.029 | INFO | __main__ | 启动 FastAPI 服务器: 127.0.0.1:8000
2025-09-29 13:11:28.030 | DEBUG | asyncio | Using proactor: IocpProactor
2025-09-29 13:11:28.059 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
2025-09-29 19:35:33.086 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
2025-09-29 19:35:33.087 | INFO | __main__ | 启动 FastAPI 服务器: 127.0.0.1:8000
2025-09-29 19:35:33.105 | DEBUG | asyncio | Using proactor: IocpProactor
2025-09-29 19:35:33.458 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
2025-09-29 21:02:28.561 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
2025-09-29 21:02:28.563 | INFO | __main__ | 启动 FastAPI 服务器: 127.0.0.1:8000
2025-09-29 21:02:28.579 | DEBUG | asyncio | Using proactor: IocpProactor
2025-09-29 21:02:28.828 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
2025-09-29 21:13:29.629 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
2025-09-29 21:13:29.631 | INFO | __main__ | 启动 FastAPI 服务器: 127.0.0.1:8000
2025-09-29 21:13:29.632 | DEBUG | asyncio | Using proactor: IocpProactor
2025-09-29 21:13:29.653 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
2025-09-29 21:26:27.378 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
2025-09-29 21:26:27.379 | INFO | __main__ | 启动 FastAPI 服务器: 127.0.0.1:8000
2025-09-29 21:26:27.382 | DEBUG | asyncio | Using proactor: IocpProactor
2025-09-29 21:26:27.404 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
2025-09-29 21:30:25.753 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}
2025-09-29 21:30:25.754 | INFO | __main__ | 启动 FastAPI 服务器: 127.0.0.1:8000
2025-09-29 21:30:25.756 | DEBUG | asyncio | Using proactor: IocpProactor
2025-09-29 21:30:25.775 | DEBUG | EAN13Recognizer | 配置加载完成: preprocess={'resize_width': 1280, 'gaussian_blur_ksize': 3, 'morphology_kernel': 17, 'sobel_ksize': 3, 'binarize': 'otsu', 'close_kernel': 21}, roi={'min_area_ratio': 0.01, 'min_wh_ratio': 2.0, 'warp_target_height': 120, 'crop_bottom_ratio': 0.25}, decoder={'sample_rows': [0.35, 0.5, 0.65], 'total_modules': 95, 'guard_tolerance': 0.35, 'peak_valley_rel_threshold': 0.2, 'engine_order': ['pyzbar', 'ean13'], 'try_invert': True, 'rotations': [0, 90, 180, 270]}

View File

@@ -267,7 +267,7 @@
| user_id | BIGINT UNSIGNED | NOT NULL | | | | user_id | BIGINT UNSIGNED | NOT NULL | | |
| name | VARCHAR(120) | NOT NULL | | 供全文检索 | | name | VARCHAR(120) | NOT NULL | | 供全文检索 |
| category_id | BIGINT UNSIGNED | YES | | | | category_id | BIGINT UNSIGNED | YES | | |
| unit_id | BIGINT UNSIGNED | NOT NULL | | | | unit_id | (已移除) | | | |
| template_id | BIGINT UNSIGNED | YES | | 关联的模板 | | template_id | BIGINT UNSIGNED | YES | | 关联的模板 |
| brand | VARCHAR(64) | YES | | | | brand | VARCHAR(64) | YES | | |
| model | VARCHAR(64) | YES | | | | model | VARCHAR(64) | YES | | |
@@ -291,8 +291,8 @@
- safe_min/safe_max: 安全库存上下限 - safe_min/safe_max: 安全库存上下限
- search_text: 聚合检索字段(触发器维护) - search_text: 聚合检索字段(触发器维护)
**Indexes**: - PRIMARY KEY: `id` - KEY: `idx_products_shop` (`shop_id`) - KEY: `idx_products_category` (`category_id`) - KEY: `idx_products_unit` (`unit_id`) - KEY: `idx_products_template` (`template_id`) - KEY: `idx_products_dedupe` (`dedupe_key`) - KEY: `idx_products_shop_blacklist` (`shop_id`,`is_blacklisted`) - FULLTEXT: `ft_products_search` (`name`,`brand`,`model`,`spec`,`search_text`) - UNIQUE: `ux_products_shop_barcode` (`shop_id`,`barcode`) - UNIQUE: `ux_products_template_name_model` (`template_id`,`name`,`model`) **Indexes**: - PRIMARY KEY: `id` - KEY: `idx_products_shop` (`shop_id`) - KEY: `idx_products_category` (`category_id`) - KEY: `idx_products_template` (`template_id`) - KEY: `idx_products_dedupe` (`dedupe_key`) - KEY: `idx_products_shop_blacklist` (`shop_id`,`is_blacklisted`) - FULLTEXT: `ft_products_search` (`name`,`brand`,`model`,`spec`,`search_text`) - UNIQUE: `ux_products_shop_barcode` (`shop_id`,`barcode`) - UNIQUE: `ux_products_template_name_model` (`template_id`,`name`,`model`)
**Foreign Keys**: - `fk_products_shop`: `shop_id``shops(id)` - `fk_products_user`: `user_id``users(id)` - `fk_products_category`: `category_id``product_categories(id)` - `fk_products_unit`: `unit_id``product_units(id)` - `fk_products_template`: `template_id``part_templates(id)` - `fk_products_globalsku`: `global_skus(id)` **Foreign Keys**: - `fk_products_shop`: `shop_id``shops(id)` - `fk_products_user`: `user_id``users(id)` - `fk_products_category`: `category_id``product_categories(id)` - `fk_products_template`: `template_id``part_templates(id)` - `fk_products_globalsku`: `global_skus(id)`
### part_submissions配件提交与审核 ### part_submissions配件提交与审核
| Column Name | Data Type | Nullable | Default | Comment | | Column Name | Data Type | Nullable | Default | Comment |
@@ -377,8 +377,9 @@
| created_by_admin_id | BIGINT UNSIGNED | YES | | 创建管理员 | | created_by_admin_id | BIGINT UNSIGNED | YES | | 创建管理员 |
| created_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | | | created_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | |
| updated_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | | | updated_at | TIMESTAMP | NOT NULL | CURRENT_TIMESTAMP | |
| deleted_at | DATETIME | YES | | 软删标记 |
**Indexes**: - PRIMARY KEY: `id` - KEY: `idx_pt_category` (`category_id`) - KEY: `idx_pt_status` (`status`) - KEY: `idx_pt_admin` (`created_by_admin_id`) **Indexes**: - PRIMARY KEY: `id` - KEY: `idx_pt_category` (`category_id`) - KEY: `idx_pt_status` (`status`) - KEY: `idx_pt_admin` (`created_by_admin_id`) - KEY: `idx_part_templates_deleted_at` (`deleted_at`)
**Foreign Keys**: - `fk_pt_category`: `category_id``product_categories(id)` - `fk_pt_admin`: `created_by_admin_id``admins(id)` **Foreign Keys**: - `fk_pt_category`: `category_id``product_categories(id)` - `fk_pt_admin`: `created_by_admin_id``admins(id)`
### part_template_params模板参数字段 ### part_template_params模板参数字段

View File

@@ -728,6 +728,30 @@ paths:
responses: responses:
'200': { description: 成功 } '200': { description: 成功 }
/api/normal-admin/application/status:
get:
summary: 普通管理员-本人申请状态查询(✅ Fully Implemented
description: 返回当前用户最近一次申请状态与是否已具备普通管理员权限。
parameters:
- in: header
name: X-User-Id
required: true
schema: { type: integer, format: int64 }
description: 当前登录用户ID
responses:
'200':
description: 成功
content:
application/json:
schema:
type: object
properties:
isNormalAdmin: { type: boolean }
applicationStatus: { type: string, enum: [none, pending, approved, rejected, revoked] }
lastAction: { type: string, nullable: true }
lastActionAt: { type: string, format: date-time, nullable: true }
lastRemark: { type: string, nullable: true }
/api/admin/normal-admin/applications: /api/admin/normal-admin/applications:
get: get:
summary: 平台-普通管理员申请列表(❌ Partially Implemented summary: 平台-普通管理员申请列表(❌ Partially Implemented
@@ -1149,12 +1173,18 @@ paths:
/api/products: /api/products:
get: get:
summary: 商品搜索(✅ Fully Implemented summary: 商品搜索(✅ Fully Implemented
description: 支持 kw/page/size/categoryId返回 {list:[]} 以兼容前端。 description: 支持 kw/page/size/categoryId/templateId 以及模板参数过滤。模板参数以 param_ 前缀传入,如 param_颜色=黑、param_内径=10后端对 JSON attributes 进行 LIKE 匹配(字符串化),多个参数为 AND 关系。返回 {list:[]} 以兼容前端。
parameters: parameters:
- in: query - in: query
name: kw name: kw
schema: schema:
type: string type: string
- in: query
name: categoryId
schema: { type: integer, format: int64 }
- in: query
name: templateId
schema: { type: integer, format: int64 }
- in: query - in: query
name: page name: page
schema: schema:
@@ -1165,6 +1195,11 @@ paths:
schema: schema:
type: integer type: integer
default: 50 default: 50
- in: query
name: param_*
schema:
type: string
description: 模板参数过滤,星号代表任意模板参数键;示例 param_颜色=黑
responses: responses:
'200': '200':
description: 成功 description: 成功
@@ -1204,11 +1239,16 @@ paths:
/api/products/{id}: /api/products/{id}:
get: get:
summary: 商品详情(✅ Fully Implemented summary: 商品详情(✅ Fully Implemented
description: 默认对软删记录返回 404仅当 includeDeleted=true 时返回已软删详情(仅管理端使用)。
parameters: parameters:
- in: path - in: path
name: id name: id
required: true required: true
schema: { type: integer } schema: { type: integer }
- in: query
name: includeDeleted
required: false
schema: { type: boolean, default: false }
responses: responses:
'200': '200':
description: 成功 description: 成功
@@ -2232,7 +2272,6 @@ paths:
spec: { type: string, nullable: true } spec: { type: string, nullable: true }
origin: { type: string, nullable: true } origin: { type: string, nullable: true }
barcode: { type: string, nullable: true } barcode: { type: string, nullable: true }
unitId: { type: integer, format: int64, nullable: true }
categoryId: { type: integer, format: int64, nullable: true } categoryId: { type: integer, format: int64, nullable: true }
parameters: { type: object, additionalProperties: true, nullable: true } parameters: { type: object, additionalProperties: true, nullable: true }
images: images:
@@ -2311,7 +2350,6 @@ paths:
spec: { type: string, nullable: true } spec: { type: string, nullable: true }
origin: { type: string, nullable: true } origin: { type: string, nullable: true }
barcode: { type: string, nullable: true } barcode: { type: string, nullable: true }
unitId: { type: integer, format: int64, nullable: true }
categoryId: { type: integer, format: int64, nullable: true } categoryId: { type: integer, format: int64, nullable: true }
parameters: { type: object, additionalProperties: true, nullable: true } parameters: { type: object, additionalProperties: true, nullable: true }
images: images:
@@ -2401,7 +2439,6 @@ paths:
name: { type: string, nullable: true } name: { type: string, nullable: true }
brand: { type: string, nullable: true } brand: { type: string, nullable: true }
spec: { type: string, nullable: true } spec: { type: string, nullable: true }
unitId: { type: integer, format: int64, nullable: true }
categoryId: { type: integer, format: int64, nullable: true } categoryId: { type: integer, format: int64, nullable: true }
parameters: { type: object, additionalProperties: true, nullable: true } parameters: { type: object, additionalProperties: true, nullable: true }
images: images:
@@ -2482,7 +2519,7 @@ paths:
/api/admin/part-templates: /api/admin/part-templates:
get: get:
summary: 管理端-模板列表(❌ Partially Implemented summary: 管理端-模板列表(✅ Fully Implemented
responses: { '200': { description: 成功 } } responses: { '200': { description: 成功 } }
post: post:
summary: 管理端-创建模板(❌ Partially Implemented summary: 管理端-创建模板(❌ Partially Implemented
@@ -2514,11 +2551,27 @@ paths:
responses: { '200': { description: 成功 } } responses: { '200': { description: 成功 } }
/api/admin/part-templates/{id}: /api/admin/part-templates/{id}:
get: get:
summary: 管理端-模板详情(❌ Partially Implemented summary: 管理端-模板详情(✅ Fully Implemented
parameters: [ { in: path, name: id, required: true, schema: { type: integer, format: int64 } } ] parameters: [ { in: path, name: id, required: true, schema: { type: integer, format: int64 } } ]
responses: { '200': { description: 成功 } } responses: { '200': { description: 成功 } }
put: put:
summary: 管理端-更新模板(❌ Partially Implemented summary: 管理端-更新模板(❌ Partially Implemented
delete:
summary: 管理端-删除模板(软删除,✅ Fully Implemented
description: |-
默认行为:软删除(隐藏)——仅将 `part_templates.status` 置为 0前台列表默认不再显示。
强制模式:`force=true` 时,执行永久删除:删除参数定义并清理关联商品(软删)与提交(软删),最后删除模板记录。
parameters:
- in: path
name: id
required: true
schema: { type: integer, format: int64 }
- in: query
name: force
required: false
schema: { type: boolean }
responses:
'200': { description: 成功 }
parameters: [ { in: path, name: id, required: true, schema: { type: integer, format: int64 } } ] parameters: [ { in: path, name: id, required: true, schema: { type: integer, format: int64 } } ]
requestBody: requestBody:
required: true required: true

View File

@@ -0,0 +1,127 @@
## 模板参数可模糊查询(±容差)功能需求文档
### 1. 背景与目标
当前用户端「按模板参数查询」要求参数值与数据库完全相同才能命中,实际使用中数值类参数(如内径、外径、长度等)存在测量/录入微小误差,严格等值导致命中率偏低。新增能力:在管理端创建模板时,为每个参数提供「可模糊查询」选项;开启后,用户搜索该参数时按数值区间匹配(±容差);未开启的参数继续精确等值。
### 2. 业务范围
- 场景:用户端/管理端的商品列表查询(含「按模板参数查询」模式)。
- 对象:模板参数定义(仅限数值型参数生效)。
- 不影响:名称/品牌/型号/规格关键字搜索逻辑;非数值类型参数的等值匹配逻辑。
### 3. 术语与约束
- 模板参数类型string/number/boolean/enum/date。
- 模糊查询仅对 type=number 生效;其他类型不展示该选项或忽略配置。
- 容差tolerance对搜索入参 v匹配区间为 \[v - tolerance, v + tolerance](闭区间)。默认容差为 1见配置项可在参数层级单独覆盖。
- 组合关系:多参数为 AND 关系;每个参数根据其「可模糊查询」与容差独立计算。
### 4. 交互与流程
- 管理端-模板配置:
- 新建/编辑模板参数时,新增选项:
- 可模糊查询(开关,仅当类型为 number 显示)
- 容差值number>0显示单位提示`unit` 字段;当开关开启时必填,否则置空)
- 校验:
- type≠number 时禁止开启;
- 容差必须为正数,支持小数;
- 可保存为“使用平台默认容差”,当字段留空时后端落默认(见配置)。
- 用户端/管理端-按模板参数查询:
- 入参与现状一致:仍以 `templateId` + 多个 `param_*` 传参;
- 行为变化:
- 对应参数若开启可模糊查询:按区间 \[v - tol, v + tol] 比较;
- 否则:仍为精确等值比较。
### 5. 数据模型变更(待实施)
- 表:`part_template_params`
- 新增列:
- `fuzzy_searchable` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否允许模糊查询(仅数值型)'
- `fuzzy_tolerance` DECIMAL(18,6) NULL COMMENT '容差NULL 表示使用平台默认容差'
- 说明:
- 仅当 `type='number' AND fuzzy_searchable=1` 时才使用容差;
- 初始迁移将全部历史记录置为 `fuzzy_searchable=0, fuzzy_tolerance=NULL`,保持现有行为不变。
### 6. 配置项(后端)
- `search.fuzzy.enabled`bool默认 true是否启用模糊查询全局开关
- `search.fuzzy.defaultTolerance`decimal默认 1.0):当参数未设置 `fuzzy_tolerance` 时使用;
- 读取途径Spring 配置application.properties/yaml或环境变量。禁止在代码中硬编码数字 1。
- 仅全局配置,不支持租户级(`system_parameters`)覆盖;无需设置小数精度上限/最大容差限制。
### 7. 接口协议与兼容性
- 查询接口:`GET /api/products`(已存在)
- 入参保持不变:`templateId``param_*`
- 语义扩展(无须 `templateId` 也启用模糊):后端将基于商品行的 `template_id` 与参数定义逐行判定某个 `param_*` 是否启用 ±容差;若该参数在对应模板中未开启模糊或非数值型,则对该条件执行等值匹配。
- 模板接口:`POST /api/admin/part-templates``PUT /api/admin/part-templates/{id}`(已存在)
- 参数定义对象新增字段:
- `fuzzySearchable`boolean
- `fuzzyTolerance`numbernullable
- 若前端暂未改造,后端默认按 `fuzzySearchable=false` 处理,兼容旧请求体。
(根据「接口规范生效条件」,待功能开发完成后更新 `doc/openapi.yaml` 中对应 schema 与描述,并在 summary/description 标注实现状态)
### 8. 后端实现要点(建议方案)
当前实现(精确匹配),示意:
```sql
-- 现状(等值):
AND JSON_UNQUOTE(JSON_EXTRACT(p.attributes_json, '$.内径')) = '10'
```
推荐实现:
- 行级判定方案(支持无 `templateId` 也启用模糊):
- 对每个传入的 `param_<key>=v`
-`EXISTS` 子查询或 `JOIN part_template_params ptp ON ptp.template_id=p.template_id AND ptp.field_key='<key>'` 获取参数定义;
-`ptp.type='number' AND ptp.fuzzy_searchable=1`:对 `v` 解析为数值,计算 `tol = COALESCE(ptp.fuzzy_tolerance, :defaultTolerance)`
- 下限截断:`lower = GREATEST(0, v - tol)`
- 条件:
```sql
CAST(JSON_UNQUOTE(JSON_EXTRACT(p.attributes_json, '$.<key>')) AS DECIMAL(18,6)) BETWEEN :lower AND :upper
```
- 否则:执行等值匹配:
```sql
JSON_UNQUOTE(JSON_EXTRACT(p.attributes_json, '$.<key>')) = :val
```
- 快路径(可选):当请求携带 `templateId` 时,可先一次性加载该模板参数定义映射到内存,按映射决定每个条件构造,以减少 `JOIN/EXISTS` 次数。
- 容差取值:优先 `ptp.fuzzy_tolerance`,否则全局 `search.fuzzy.defaultTolerance`。
性能建议:
- 初期:允许全表扫描 + JSON_EXTRACT观察真实 QPS 与延迟;
- 进阶(可选):对热点参数引入“生成列 + 索引”Generated Column例如
- 在 `products` 增加 `attr_<key> DECIMAL(18,6) GENERATED ALWAYS AS (CAST(JSON_UNQUOTE(JSON_EXTRACT(attributes_json, '$.<key>')) AS DECIMAL(18,6))) STORED` 并建索引,以支持范围查询;
- 仅对访问量高的少数参数启用,避免列爆炸。
### 9. 管理端实现要点UI/校验)
- `admin/src/views/parts/Templates.vue`
- 参数编辑行新增:
- 开关:可模糊查询(仅 type=number 显示)
- 数值输入:容差(显示单位,>0支持小数留空表示使用平台默认
- 保存/加载兼容:与后端新增字段映射,历史数据默认显示为关闭态。
- 校验:当参数开启模糊时,对应值在 UI 侧仅允许数字输入;单位提示与 `unit` 一致。
### 10. 验收标准Test Cases
- 单参数-模糊:模板字段 `内径`numberfuzzy=truetolerance=1商品 A/B/C 分别取值 9/10/11搜索 `param_内径=10` 命中 A/B/C。
- 单参数-精确:同上但 fuzzy=false搜索 `param_内径=10` 仅命中 B。
- 多参数组合:`内径`fuzzy=true, tol=0.5)、`长度`fuzzy=false搜索 `param_内径=10`、`param_长度=20` 仅命中满足区间与等值的交集。
- 无 templateId也启用模糊后端逐行按 `p.template_id` 与参数定义判定是否应用容差。
- 容差来源:当 `fuzzy_tolerance=NULL` 时,生效平台默认容差;覆盖值生效优先级高于默认。
- 非数值参数:即使请求携带 `param_颜色=黑`,也严格等值。
- 下限截断:当 `v - tol < 0` 时,以 `0` 作为下限;不支持负数参数匹配。
- 非法输入:当参数在模板中开启模糊但请求值非数字时,返回 400Bad Request
### 11. 兼容与回退
- 不改动现有请求入参与返回体,历史客户端无需升级亦可按原精确逻辑使用;
- 新能力由模板参数配置显式开启,可随时在模板中关闭;
- 如需全局关闭,可通过 `search.fuzzy.enabled=false` 临时禁用(后端配置)。
### 12. 风险与注意事项
- 数据质量:历史 `attributes_json` 中数值可能以字符串存储;需统一以 `CAST(JSON_UNQUOTE(...))` 解析。
- 单位与容差UI 需提示单位;容差与单位一一对应,避免“毫米 vs 厘米”误解。
- 性能:范围查询较等值更难走索引;必要时引入“生成列+索引”优化热点字段。
- 负数与边界:不支持负数参数;区间采用闭区间,且下限截断为 `0`。
### 13. 实施清单(参考)
1) 数据库:为 `part_template_params` 增列 `fuzzy_searchable`、`fuzzy_tolerance`;(变更需通过 MysqlMCP成功后同步更新 `doc/database_documentation.md` 与 `backend/db/db.sql`
2) 配置:新增 `search.fuzzy.*` 配置项并给出默认值(全局生效,无租户级覆盖);
3) 管理端:模板参数编辑 UI 新增开关与容差输入;
4) 后端:按 8 节改造查询 SQL 构建逻辑(无 `templateId` 也启用模糊,行级按模板判定);
5) 文档:在功能开发完成后更新 `doc/openapi.yaml` 中模板参数 schema 与 `GET /api/products` 的查询规则说明并标注实现状态
6) 发布前后端同步上线无需灰度与回滚开关
7) 验收 10 节用例覆盖单测/集成测试与手工回归

View File

@@ -0,0 +1,175 @@
## 货品删除功能开发文档(软删方案)
### 1. 背景与目标
- 将“与货品相关”的删除行为统一为软删除,避免历史引用断裂,支持后续恢复与审计。
- 用户仅保留“拉黑/恢复”,订单维持“作废 void”不做删除。
### 2. 范围
- 货品主表:`products`
- 关联信息:`product_images``product_prices``inventories``product_aliases`
- 相关查询接口:商品搜索、详情、导出(如有)
### 2.1 父子级联关系(必须遵守)
- 分类(`product_categories` → 模板(`part_templates` → 商品(`products`
- 规则:
- 删除分类 ⇒ 级联软删该分类下所有模板;再级联软删由这些模板创建的所有商品;并同时软删所有 `category_id=该分类` 的商品(包括未通过模板创建的商品)。
- 删除模板 ⇒ 仅软删该模板下的商品,不影响同分类其它模板的商品。
- 订单不可删除仅允许作废void因此采用“软删”是必要前提避免历史订单断裂。
- 恢复:当前不提供任何恢复入口;如未来开放,恢复不做级联,需逐层独立恢复以避免误恢复。
### 3. 设计要点
- 软删标记:使用 `products.deleted_at DATETIME NULL`(已存在)。被软删即视为“不对外可见”。
- 恢复:当前不提供恢复入口。若未来开放,语义为将 `deleted_at=NULL`
- 查询默认过滤:所有列表/搜索默认附加 `deleted_at IS NULL`(当前搜索已实现)。
- 详情访问:若记录被软删,返回 404或通过 `includeDeleted=true` 显式读取)。
- 关联表处理:软删商品时不物理删除图片/价格/库存/别名(均按商品引用读取,详情被 404 屏蔽即可)。
- 模板软删标记统一:为 `part_templates` 引入 `deleted_at DATETIME NULL` 以统一软删标记;`status` 字段保留为启停用,不代表软删。所有查询需同时过滤 `deleted_at IS NULL AND status=1`(按需)。
- 字典与作用域:分类与单位属于 `shop_id=0` 的全局字典。删除分类会影响所有店铺下此分类的模板与商品;此操作需平台管理员权限并要求二次确认。
- 报表与搜索:默认排除软删记录;不提供“含回收站”开关。
- 数据保留与清理:支持配置项 `SOFT_DELETE_RETENTION_DAYS`(默认永久保留,仅清理无引用对象)。
- 单位删除校验:移除对已废弃 `products.unit_id` 的校验逻辑。
### 4. 数据库与索引
现状:`products` 存在唯一约束 `UNIQUE(shop_id, barcode)`。软删后可能需要“同店铺、同条码”重新建商品。
- 目标:唯一约束仅作用于“活动记录”(未软删)。
- 做法:增加生成列 `is_active` 并重建唯一索引MySQL 8
DDL上线脚本草案
```sql
-- 仅对生产环境执行一次;如已存在请跳过对应步骤
ALTER TABLE products
ADD COLUMN is_active TINYINT AS (CASE WHEN deleted_at IS NULL THEN 1 ELSE 0 END) STORED,
ADD INDEX idx_products_deleted_at (deleted_at);
-- 重建唯一索引,使其仅约束未软删记录
DROP INDEX ux_products_shop_barcode ON products; -- 若不存在请忽略
CREATE UNIQUE INDEX ux_products_shop_barcode_live ON products(shop_id, barcode, is_active);
```
风险与说明
- “条码为空”不会受唯一约束影响MySQL 对 NULL 不唯一);符合预期。
- 老数据不受影响;后续删除改为软删即可。
- 若未来需要“永久删除”,可新增仅限平台运维的强删脚本,先清理关联,再物理删除目标商品。
- 如未来开放“恢复”,当恢复商品与现存“活动记录”在 `(shop_id, barcode)` 上冲突时,恢复应返回 `409 Conflict` 并附带冲突商品信息。
模板表 DDL新增软删标记
```sql
ALTER TABLE part_templates
ADD COLUMN deleted_at DATETIME NULL,
ADD INDEX idx_part_templates_deleted_at (deleted_at);
```
### 5. 接口设计OpenAPI 约定)
说明:按规范,等后端开始开发即补充到 `/doc/openapi.yaml` 并标注实现状态;本方案不新增任何“恢复”接口。
1) 软删商品(行为不变,明确语义)
- Method/Path: `DELETE /api/products/{id}`
- 语义:软删,将 `deleted_at=NOW()`
- 返回:`200 {}`
- 鉴权:需要 `X-Shop-Id`/`X-User-Id` 或 Token且仅允许同店铺数据。
2) 商品详情(行为调整)
- Method/Path: `GET /api/products/{id}`
- 默认:若 `deleted_at IS NOT NULL` 返回 `404`
- 可选:`includeDeleted=true` 时允许读取已软删详情(仅管理端使用)。
3) 恢复接口
- 不同意新增以下恢复接口:`PUT /api/admin/dicts/categories/{id}/restore``PUT /api/admin/part-templates/{id}/restore``PUT /api/products/{id}/restore`
### 6. 后端实现说明
- Controller 改动(示意)
- `ProductController.delete(id, shopId)`:保持现有调用,内部执行软删。
- `GET /api/products/{id}`:调用 `productService.findDetail(id)` 前,先判断 `deleted_at`,若非空且未显式 `includeDeleted``404`
- Service 改动(核心)
- 移除/不提供任何恢复相关方法。
- `findDetail(id)`:若被软删且无 `includeDeleted` 参数 → 返回空 Optional。
- 模板表采用 `deleted_at` 表示软删,`status` 表示启停用;查询需同时过滤 `deleted_at IS NULL` 与必要的 `status` 条件。
#### 6.1 级联软删伪代码
```java
// 分类软删
void deleteCategorySoft(Long categoryId) {
// 1) 标记分类 deleted_at
UPDATE product_categories SET deleted_at=NOW() WHERE id=? AND deleted_at IS NULL;
// 2) 级联模板软删(统一使用 deleted_at
UPDATE part_templates SET deleted_at=NOW() WHERE category_id=? AND deleted_at IS NULL;
// 3) 级联商品软删:模板创建的商品 + 直接挂在分类下的商品
UPDATE products SET deleted_at=NOW() WHERE (
template_id IN (SELECT id FROM part_templates WHERE category_id=?)
OR category_id=?
) AND deleted_at IS NULL;
}
// 模板软删(不波及其它模板)
void deleteTemplateSoft(Long templateId) {
// 1) 模板标记为软删
UPDATE part_templates SET deleted_at=NOW() WHERE id=? AND deleted_at IS NULL;
// 2) 级联商品软删(仅该模板下)
UPDATE products SET deleted_at=NOW() WHERE template_id=? AND deleted_at IS NULL;
}
```
### 7. 前端改动
- 列表页:保持不显示软删项(现已过滤)。
- 详情页:若接口返回 404提示“已被删除或无权限”。
- 管理端:不提供“回收站/恢复”入口;删除按钮提示:该操作为软删除,对前台不可见,当前无恢复入口。
### 8. 权限与审计
- 鉴权:沿用现有用户/店铺头部识别;仅同店铺商品可操作。
- 权限边界:
- 普通用户:仅可删除本用户的货品;无权删除模板与分类;无恢复权限。
- 店铺管理员:仅有审核功能;无删除模板/分类与恢复权限。
- 平台管理员:可删除货品、模板、分类;删除全局分类需二次确认;无恢复权限。
- 审计:不记录操作日志(操作者、时间、来源 IP、对象 ID 与名称),以简化开发。
### 9. 测试用例
- 删除后搜索不可见;`GET /api/products/{id}` 返回 404。
- 条码唯一:软删后允许同店铺同条码新建。
- (如未来开放恢复)恢复时如与现有活动记录冲突,返回 409 并附带冲突商品信息。
### 10. 发布与回滚
- 发布顺序:
1) 执行数据库 DDL生成列与索引
2) 上线后端(调整 detail 行为,移除/不提供恢复逻辑)。
3) 上线前端(不提供回收站/恢复入口)。
- 回滚:
- 后端回滚到旧版本DDL 不需要回退(生成列与新索引向前兼容)。
### 11. FAQ / 风险
- 问:软删后图片与价格是否清理?
- 答:不清理,保持数据可恢复;若永久删除再统一清理关联。
- 问:库存与统计是否包含软删商品?
- 答:常规统计应排除软删;如需包含,增加显式参数。
- 问:条码冲突如何处理?
- 答:按“活动记录”唯一;如未来开放恢复,发现冲突则返回 409并指明冲突商品。
- 问:字典(分类/单位)是否为全局维度?删除是否影响所有店铺?
- 答:是,`shop_id=0` 全局字典;删除全局分类会影响所有店铺下该分类的模板与商品,需平台管理员二次确认。
- 问:是否保留“强删”入口?
- 答:保留仅限平台运维的强删入口(默认关闭)。分类/模板强删前需校验无订单关联商品后再执行。
- 问:为何不做物理删除?
- 答:订单/流水等历史记录必须可追溯;物理删除会破坏外键与统计。软删能满足“前台不可见、后台可恢复”的业务诉求。
### 12. 任务拆解(实施)
- 后端:
- [ ] `GET /api/products/{id}` 软删返回 404 / 支持 `includeDeleted`
- [ ] 分类删除级联扩展:同时软删 `category_id=该分类` 的商品(含未走模板创建)
- [ ] 模板表引入 `deleted_at`;查询同时过滤 `deleted_at IS NULL` 与必要的 `status`
- [ ] 移除“单位删除校验检查 products.unit_id”的逻辑
- 数据库:
- [ ]`products` 增加 `is_active` 与唯一索引(见 DDL
- [ ]`part_templates` 增加 `deleted_at` 与索引
- 前端管理端:
- [ ] 删除按钮文案更新(软删除,对前台不可见,当前无恢复入口)
- [ ] 不提供“回收站/恢复”入口
(本文件为技术方案与实施指引,变更上线后请同步 `/doc/openapi.yaml``/doc/database_documentation.md`

1
frontend/.env.local Normal file
View File

@@ -0,0 +1 @@
VITE_APP_API_BASE_URL=http://192.168.31.192:8080

View File

@@ -49,3 +49,8 @@ export const KPI_ICONS = {
stockCount: '/static/icons/product.png' stockCount: '/static/icons/product.png'
} }
// 登录页顶部图片,避免在页面硬编码,允许通过环境变量或本地存储覆盖
const envAuthLoginImg = (typeof process !== 'undefined' && process.env && (process.env.VITE_APP_AUTH_LOGIN_TOP_IMAGE || process.env.AUTH_LOGIN_TOP_IMAGE)) || '';
const storageAuthLoginImg = typeof uni !== 'undefined' ? (uni.getStorageSync('AUTH_LOGIN_TOP_IMAGE') || '') : '';
export const AUTH_LOGIN_TOP_IMAGE = String(envAuthLoginImg || storageAuthLoginImg || '/static/icons/undraw_visual-data_1eya.png');

View File

@@ -42,3 +42,11 @@ export const STORAGE_KEYS = {
} }
// KPI 标签常量,避免页面硬编码
export const KPI_LABELS = {
todaySales: '今日销售额',
monthSales: '本月销售额',
monthProfit: '本月利润',
stockCount: '库存量'
}

View File

@@ -38,11 +38,23 @@
} }
}, },
{ {
"path": "pages/product/form", "path": "pages/product/submission-detail",
"style": { "style": {
"navigationBarTitleText": "编辑货品" "navigationBarTitleText": "提交详情"
} }
}, },
{
"path": "pages/product/form",
"style": {
"navigationBarTitleText": "编辑货品"
}
},
{
"path": "pages/product/product-detail",
"style": {
"navigationBarTitleText": "货品详情"
}
},
{ {
"path": "pages/product/categories", "path": "pages/product/categories",
"style": { "style": {

View File

@@ -1,18 +1,21 @@
<template> <template>
<view class="auth-page"> <view class="auth-page">
<view class="tabs"> <view class="login-hero">
<view :class="['tab', tab==='login'?'active':'']" @click="tab='login'">登录</view> <image class="login-hero-img" :src="authLoginTopImage" mode="widthFix" />
<view :class="['tab', tab==='register'?'active':'']" @click="tab='register'">注册</view>
<view :class="['tab', tab==='reset'?'active':'']" @click="tab='reset'">忘记密码</view>
</view> </view>
<view class="header"><text class="title">邮箱密码登录</text></view>
<view v-if="tab==='login'" class="panel"> <view v-if="tab==='login'" class="panel">
<input class="input" type="text" v-model.trim="loginForm.email" placeholder="输入邮箱" /> <input class="input" type="text" v-model.trim="loginForm.email" placeholder="输入邮箱" />
<input class="input" type="password" v-model="loginForm.password" placeholder="输入密码" /> <input class="input" type="password" v-model="loginForm.password" placeholder="输入密码" />
<button class="btn primary" :disabled="loading" @click="onLogin">登录</button> <button class="btn primary" :disabled="loading" @click="onLogin">登录</button>
<view class="quick-inline">
<button class="quick-link" @click="gotoRegister">注册</button>
<button class="quick-link" @click="gotoReset">忘记密码</button>
</view>
</view> </view>
<view v-else-if="tab==='register'" class="panel"> <view class="panel minor" v-if="tab==='register'">
<input class="input" type="text" v-model.trim="regForm.name" placeholder="输入用户名" /> <input class="input" type="text" v-model.trim="regForm.name" placeholder="输入用户名" />
<input class="input" type="text" v-model.trim="regForm.email" placeholder="输入邮箱" /> <input class="input" type="text" v-model.trim="regForm.email" placeholder="输入邮箱" />
<view class="row"> <view class="row">
@@ -24,7 +27,7 @@
<button class="btn primary" :disabled="loading" @click="onRegister">注册新用户</button> <button class="btn primary" :disabled="loading" @click="onRegister">注册新用户</button>
</view> </view>
<view v-else class="panel"> <view class="panel minor" v-if="tab==='reset'">
<input class="input" type="text" v-model.trim="resetForm.email" placeholder="输入邮箱" /> <input class="input" type="text" v-model.trim="resetForm.email" placeholder="输入邮箱" />
<view class="row"> <view class="row">
<input class="input flex1" type="text" v-model.trim="resetForm.code" placeholder="邮箱验证码" /> <input class="input flex1" type="text" v-model.trim="resetForm.code" placeholder="邮箱验证码" />
@@ -35,16 +38,19 @@
<button class="btn primary" :disabled="loading" @click="onReset">重置密码</button> <button class="btn primary" :disabled="loading" @click="onReset">重置密码</button>
</view> </view>
</view> </view>
</template> </template>
<script> <script>
import { get, post } from '../../common/http.js' import { get, post } from '../../common/http.js'
import { AUTH_LOGIN_TOP_IMAGE } from '../../common/config.js'
export default { export default {
data(){ data(){
return { return {
loading: false, loading: false,
tab: 'login', tab: 'login',
authLoginTopImage: AUTH_LOGIN_TOP_IMAGE,
loginForm: { email: '', password: '' }, loginForm: { email: '', password: '' },
regForm: { name: '', email: '', code: '', password: '', password2: '' }, regForm: { name: '', email: '', code: '', password: '', password2: '' },
resetForm: { email: '', code: '', password: '', password2: '' }, resetForm: { email: '', code: '', password: '', password2: '' },
@@ -55,6 +61,8 @@ export default {
}, },
beforeUnmount(){ this._timers.forEach(t=>clearInterval(t)) }, beforeUnmount(){ this._timers.forEach(t=>clearInterval(t)) },
methods: { methods: {
gotoRegister(){ this.tab='register' },
gotoReset(){ this.tab='reset' },
toast(msg){ try{ uni.showToast({ title: String(msg||'操作失败'), icon: 'none' }) } catch(_){} }, toast(msg){ try{ uni.showToast({ title: String(msg||'操作失败'), icon: 'none' }) } catch(_){} },
validateEmail(v){ return /^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/.test(String(v||'').trim()) }, validateEmail(v){ return /^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/.test(String(v||'').trim()) },
startCountdown(key){ startCountdown(key){
@@ -136,20 +144,39 @@ export default {
}catch(e){ this.toast(e.message) } }catch(e){ this.toast(e.message) }
finally{ this.loading=false } finally{ this.loading=false }
} }
} }
} }
</script> </script>
<style lang="scss"> <style lang="scss">
.auth-page{ padding: 32rpx; display:flex; flex-direction: column; gap: 24rpx; } @import '../../uni.scss';
.tabs{ display:flex; gap: 24rpx; } .auth-page{ padding: 32rpx; display:flex; flex-direction: column; gap: 24rpx; position: relative; min-height: 100vh; }
.tab{ padding: 12rpx 20rpx; border-radius: 999rpx; background:#f2f4f8; color:#5b6b80; font-weight:700; } .header{ display:flex; align-items:center; justify-content:center; padding: 8rpx 0 0; }
.tab.active{ background:#2d6be6; color:#fff; } .title{ font-size: 34rpx; font-weight: 800; color: $uni-text-color; }
.panel{ display:flex; flex-direction: column; gap: 16rpx; background:#fff; padding: 24rpx; border-radius: 16rpx; border:2rpx solid #eef2f9; } .login-hero{ display:flex; justify-content:center; padding: 16rpx 0 0; }
.input{ background:#f7f9ff; border:2rpx solid rgba(45,107,230,0.12); border-radius: 12rpx; padding: 22rpx 20rpx; font-size: 28rpx; } .login-hero-img{ width: 72%; max-width: 560rpx; border-radius: 8rpx; }
.panel{ display:flex; flex-direction: column; gap: 16rpx; background: transparent; padding: 0; border-radius: 0; border: none; }
.panel.minor{ margin-top: 12rpx; }
.input{ background:#ffffff; border:2rpx solid #e5e7eb; border-radius: 12rpx; padding: 22rpx 20rpx; font-size: 28rpx; }
.row{ display:flex; gap: 12rpx; align-items:center; } .row{ display:flex; gap: 12rpx; align-items:center; }
.flex1{ flex:1; } .flex1{ flex:1; }
.btn{ padding: 22rpx 20rpx; border-radius: 12rpx; font-weight: 800; text-align:center; } .btn{ padding: 22rpx 20rpx; border-radius: 12rpx; font-weight: 800; text-align:center; }
.btn.primary{ background: linear-gradient(135deg, #4788ff 0%, #2d6be6 100%); color:#fff; } .btn.primary{ background: linear-gradient(135deg, #4788ff 0%, #2d6be6 100%); color:#fff; border: 1rpx solid rgba(45,107,230,0.25); width: 72%; margin: 0 auto; padding: 14rpx 16rpx; }
.btn.ghost{ background:#eef3ff; color:#2d6be6; } .btn.ghost{ background:#eef3ff; color:#2d6be6; }
/* 右下角快捷入口:贴着登录功能,无边框、无背景 */
.quick-inline{ display:flex; gap: 28rpx; justify-content:flex-end; align-items:center; margin-top: 10rpx; }
.quick-link{ background: transparent !important; color: #2d6be6; border: none !important; outline: none; padding: 0; font-size: 26rpx; font-weight: 700; box-shadow: none; line-height: 1.2; }
.quick-link::after{ border: none !important; }
/* 注册/重置页:验证码按钮与左侧输入框等高,且更紧凑 */
.panel.minor .row > .input{ height: $app-form-control-height; padding: 0 $app-form-control-padding-x; }
.panel.minor .row > .btn.ghost{
height: $app-form-control-height;
padding: 0 $app-form-control-padding-x;
border-radius: $app-form-control-border-radius;
display: inline-flex;
align-items: center;
justify-content: center;
}
</style> </style>

View File

@@ -12,28 +12,28 @@
<view class="kpi-item kpi-card"> <view class="kpi-item kpi-card">
<image :src="KPI_ICONS.todaySales" class="kpi-icon" mode="aspectFit"></image> <image :src="KPI_ICONS.todaySales" class="kpi-icon" mode="aspectFit"></image>
<view class="kpi-content"> <view class="kpi-content">
<text class="kpi-label">今日销售额</text> <text class="kpi-label">{{ KPI_LABELS.todaySales }}</text>
<text class="kpi-value">{{ kpi.todaySales }}</text> <text class="kpi-value">{{ kpi.todaySales }}</text>
</view> </view>
</view> </view>
<view class="kpi-item kpi-card"> <view class="kpi-item kpi-card">
<image :src="KPI_ICONS.monthSales" class="kpi-icon" mode="aspectFit"></image> <image :src="KPI_ICONS.monthSales" class="kpi-icon" mode="aspectFit"></image>
<view class="kpi-content"> <view class="kpi-content">
<text class="kpi-label">本月销售额</text> <text class="kpi-label">{{ KPI_LABELS.monthSales }}</text>
<text class="kpi-value">{{ kpi.monthSales }}</text> <text class="kpi-value">{{ kpi.monthSales }}</text>
</view> </view>
</view> </view>
<view class="kpi-item kpi-card"> <view class="kpi-item kpi-card">
<image :src="KPI_ICONS.monthProfit" class="kpi-icon" mode="aspectFit"></image> <image :src="KPI_ICONS.monthProfit" class="kpi-icon" mode="aspectFit"></image>
<view class="kpi-content"> <view class="kpi-content">
<text class="kpi-label">本月利润</text> <text class="kpi-label">{{ KPI_LABELS.monthProfit }}</text>
<text class="kpi-value">{{ kpi.monthProfit }}</text> <text class="kpi-value">{{ kpi.monthProfit }}</text>
</view> </view>
</view> </view>
<view class="kpi-item kpi-card"> <view class="kpi-item kpi-card">
<image :src="KPI_ICONS.stockCount" class="kpi-icon" mode="aspectFit"></image> <image :src="KPI_ICONS.stockCount" class="kpi-icon" mode="aspectFit"></image>
<view class="kpi-content"> <view class="kpi-content">
<text class="kpi-label">库存商品数量</text> <text class="kpi-label">{{ KPI_LABELS.stockCount }}</text>
<text class="kpi-value">{{ kpi.stockCount }}</text> <text class="kpi-value">{{ kpi.stockCount }}</text>
</view> </view>
</view> </view>
@@ -93,12 +93,13 @@
<script> <script>
import { get, post, put } from '../../common/http.js' import { get, post, put } from '../../common/http.js'
import { ROUTES } from '../../common/constants.js' import { ROUTES, KPI_LABELS } from '../../common/constants.js'
import { KPI_ICONS as KPI_ICON_MAP } from '../../common/config.js' import { KPI_ICONS as KPI_ICON_MAP } from '../../common/config.js'
export default { export default {
data() { data() {
return { return {
KPI_ICONS: KPI_ICON_MAP, KPI_ICONS: KPI_ICON_MAP,
KPI_LABELS,
kpi: { todaySales: '0.00', monthSales: '0.00', monthProfit: '0.00', stockCount: '0' }, kpi: { todaySales: '0.00', monthSales: '0.00', monthProfit: '0.00', stockCount: '0' },
activeTab: 'home', activeTab: 'home',
notices: [], notices: [],
@@ -238,6 +239,11 @@
// 报表非 tab 页,使用 navigateTo 进入 // 报表非 tab 页,使用 navigateTo 进入
uni.navigateTo({ url: ROUTES.report }) uni.navigateTo({ url: ROUTES.report })
return return
}
if (item.key === 'vip') {
// 跳转“我的 - VIP会员”页面
uni.navigateTo({ url: '/pages/my/vip' })
return
} }
if (item.key === 'otherPay') { if (item.key === 'otherPay') {
// 进入开单页并预选“其他支出” // 进入开单页并预选“其他支出”

View File

@@ -222,9 +222,16 @@ export default {
formatDisplay(value) { formatDisplay(value) {
if (!value) return '-' if (!value) return '-'
const s = String(value) const s = String(value)
// 简单规范化:只保留到分钟 // 仅显示“YYYY-MM-DD”
const m = s.match(/^(\d{4}-\d{2}-\d{2})([ T](\d{2}:\d{2}))/) const m = s.match(/^(\d{4}-\d{2}-\d{2})/)
if (m) return `${m[1]} ${m[3]}` if (m) return m[1]
const d = new Date(s)
if (!isNaN(d.getTime())) {
const y = d.getFullYear()
const mo = String(d.getMonth() + 1).padStart(2, '0')
const da = String(d.getDate()).padStart(2, '0')
return `${y}-${mo}-${da}`
}
return s return s
}, },
startLogin() { startLogin() {

View File

@@ -46,6 +46,17 @@
</view> </view>
</view> </view>
<!-- 已是VIP展示申请普通管理员入口 -->
<view v-if="isVip" class="apply-card">
<view class="apply-text">
<text class="apply-title">申请成为普通管理员</text>
<text class="apply-desc">在普通管理端参与配件审核</text>
</view>
<view role="button" :class="['apply-btn', { disabled: applyDisabled }]" @click="onApplyNormalAdmin">
<text>{{ applyBtnText }}</text>
</view>
</view>
<view v-if="!isVip" class="purchase-card"> <view v-if="!isVip" class="purchase-card">
<view class="purchase-text"> <view class="purchase-text">
<text class="purchase-title">立即升级 VIP</text> <text class="purchase-title">立即升级 VIP</text>
@@ -67,21 +78,59 @@ export default {
isVip: false, isVip: false,
expire: '', expire: '',
price: 0, price: 0,
benefits: [] benefits: [],
normalAdmin: { isNormalAdmin: false, applicationStatus: 'none' }
} }
}, },
onShow(){ onShow(){
this.loadVip() this.loadVip()
this.loadNormalAdminStatus()
this.composeBenefits() this.composeBenefits()
}, },
computed: { computed: {
expireDisplay(){ expireDisplay(){
const s = String(this.expire || '') const v = this.expire
return s || '11年11月11日' if (v === null || v === undefined) return ''
if (typeof v === 'number') {
const d = new Date(v)
if (!isNaN(d.getTime())) {
const y = d.getFullYear()
const m = String(d.getMonth() + 1).padStart(2, '0')
const dd = String(d.getDate()).padStart(2, '0')
return `${y}-${m}-${dd}`
}
return ''
}
const s = String(v)
const m = s.match(/^(\d{4}-\d{2}-\d{2})/)
if (m) return m[1]
const idx = s.search(/[ T]/)
if (idx > 0) {
const head = s.slice(0, idx)
if (head) return head
}
const d2 = new Date(s)
if (!isNaN(d2.getTime())) {
const y = d2.getFullYear()
const m2 = String(d2.getMonth() + 1).padStart(2, '0')
const dd2 = String(d2.getDate()).padStart(2, '0')
return `${y}-${m2}-${dd2}`
}
return s
}, },
priceDisplay(){ priceDisplay(){
const n = Number(this.price) const n = Number(this.price)
return Number.isFinite(n) && n > 0 ? n.toFixed(2) : '0.00' return Number.isFinite(n) && n > 0 ? n.toFixed(2) : '0.00'
},
applyDisabled(){
const s = String(this.normalAdmin?.applicationStatus || 'none')
return !!(this.normalAdmin?.isNormalAdmin || s === 'approved' || s === 'pending')
},
applyBtnText(){
if (this.normalAdmin?.isNormalAdmin || this.normalAdmin?.applicationStatus === 'approved') return '已通过'
if (this.normalAdmin?.applicationStatus === 'pending') return '审核中'
if (!this.isVip) return '仅限VIP'
return '提交申请'
} }
}, },
methods: { methods: {
@@ -103,6 +152,17 @@ export default {
this.isVip = false this.isVip = false
} }
}, },
async loadNormalAdminStatus(){
try {
const data = await get('/api/normal-admin/application/status')
this.normalAdmin = {
isNormalAdmin: !!data?.isNormalAdmin,
applicationStatus: String(data?.applicationStatus || 'none')
}
} catch(e) {
this.normalAdmin = { isNormalAdmin: false, applicationStatus: 'none' }
}
},
async onPay(){ async onPay(){
try { try {
await post('/api/vip/pay', {}) await post('/api/vip/pay', {})
@@ -111,7 +171,20 @@ export default {
} catch(e) { } catch(e) {
uni.showToast({ title: String(e.message || '开通失败'), icon: 'none' }) uni.showToast({ title: String(e.message || '开通失败'), icon: 'none' })
} }
} },
async onApplyNormalAdmin(){
if (this.applyDisabled) {
const msg = this.normalAdmin?.isNormalAdmin || this.normalAdmin?.applicationStatus === 'approved' ? '已通过,无需重复申请' : (this.normalAdmin?.applicationStatus === 'pending' ? '审核中,请耐心等待' : '不可申请')
return uni.showToast({ title: msg, icon: 'none' })
}
try {
await post('/api/normal-admin/apply', { remark: '从我的-会员发起申请' })
uni.showToast({ title: '申请已提交', icon: 'success' })
await this.loadNormalAdminStatus()
} catch(e) {
uni.showToast({ title: String(e.message || '申请失败'), icon: 'none' })
}
}
} }
} }
</script> </script>
@@ -186,6 +259,9 @@ page {
border-color: #4c8dff; border-color: #4c8dff;
} }
/* 指定 hero 内激活态徽标文本为黑色 */
.vip-hero .status-pill.active text { color: #000 !important; }
.vip-summary { .vip-summary {
display: grid; display: grid;
grid-template-columns: repeat(2, minmax(0,1fr)); grid-template-columns: repeat(2, minmax(0,1fr));
@@ -337,6 +413,45 @@ page {
opacity: 0.88; opacity: 0.88;
} }
.apply-card {
margin-top: 0;
background: linear-gradient(135deg, rgba(30,173,145,0.14) 0%, rgba(30,173,145,0.06) 100%);
border-radius: 28rpx;
padding: 30rpx 28rpx;
display: flex;
align-items: center;
gap: 24rpx;
border: 2rpx solid rgba(30,173,145,0.18);
box-shadow: 0 10rpx 24rpx rgba(30,173,145,0.15);
}
.apply-text { flex: 1; display:flex; flex-direction: column; gap: 10rpx; }
.apply-title { font-size: 32rpx; font-weight: 800; color: #1ead91; }
.apply-desc { font-size: 24rpx; color: #247a66; line-height: 34rpx; }
.apply-btn {
flex: 0 0 auto;
padding: 20rpx 36rpx;
border-radius: 999rpx;
border: none;
background-color: transparent;
background: linear-gradient(135deg, #1ead91 0%, #159b7e 100%);
color: #fff;
font-size: 28rpx;
font-weight: 700;
box-shadow: 0 10rpx 22rpx rgba(21,155,126,0.20);
}
.apply-btn::after { border: none; }
.apply-btn:active { opacity: .9; }
.apply-btn.disabled {
opacity: .5;
background: #c7e8df;
color: #fff;
box-shadow: none;
pointer-events: none;
}
@media (max-width: 375px) { @media (max-width: 375px) {
.vip-summary { .vip-summary {
grid-template-columns: 1fr; grid-template-columns: 1fr;

View File

@@ -31,41 +31,18 @@
<view class="row"> <view class="row">
<input v-model.trim="form.spec" placeholder="规格" /> <input v-model.trim="form.spec" placeholder="规格" />
</view> </view>
<!-- 隐藏产地输入 -->
<!-- 隐藏主单位选择 -->
<view class="row"> <view class="row">
<input v-model.trim="form.origin" placeholder="产地" />
</view>
<view class="row">
<picker mode="selector" :range="unitNames" @change="onPickUnit">
<view class="picker">主单位{{ unitLabel }}</view>
</picker>
<picker mode="selector" :range="categoryNames" @change="onPickCategory"> <picker mode="selector" :range="categoryNames" @change="onPickCategory">
<view class="picker">类别{{ categoryLabel }}</view> <view class="picker">类别{{ categoryLabel }}</view>
</picker> </picker>
</view> </view>
</view> </view>
<view class="section"> <!-- 隐藏库存与安全库存输入 -->
<view class="row">
<text class="label">库存与安全库存</text>
</view>
<view class="row">
<input type="number" v-model.number="form.stock" placeholder="当前库存" />
<input type="number" v-model.number="form.safeMin" placeholder="安全库存下限" />
<input type="number" v-model.number="form.safeMax" placeholder="安全库存上限" />
</view>
</view>
<view class="section"> <!-- 隐藏价格相关输入 -->
<view class="row">
<text class="label">价格进价/零售/批发/大单</text>
</view>
<view class="row prices">
<input type="number" v-model.number="form.purchasePrice" placeholder="进货价" />
<input type="number" v-model.number="form.retailPrice" placeholder="零售价" />
<input type="number" v-model.number="form.wholesalePrice" placeholder="批发价" />
<input type="number" v-model.number="form.bigClientPrice" placeholder="大单价" />
</view>
</view>
<view class="section"> <view class="section">
<text class="label">图片</text> <text class="label">图片</text>
@@ -94,14 +71,11 @@ export default {
return { return {
id: '', id: '',
form: { form: {
name: '', barcode: '', brand: '', model: '', spec: '', origin: '', name: '', barcode: '', brand: '', model: '', spec: '',
categoryId: '', unitId: '', categoryId: '',
stock: null, safeMin: null, safeMax: null,
purchasePrice: null, retailPrice: null, wholesalePrice: null, bigClientPrice: null,
images: [], remark: '', images: [], remark: '',
platformStatus: '', sourceSubmissionId: '' platformStatus: '', sourceSubmissionId: ''
}, },
units: [],
categories: [], categories: [],
keyboardHeight: 0 keyboardHeight: 0
} }
@@ -115,12 +89,7 @@ export default {
this.disposeKeyboardListener() this.disposeKeyboardListener()
}, },
computed: { computed: {
unitNames() { return this.units.map(u => u.name) },
categoryNames() { return this.categories.map(c => c.name) }, categoryNames() { return this.categories.map(c => c.name) },
unitLabel() {
const u = this.units.find(x => String(x.id) === String(this.form.unitId))
return u ? u.name : '选择单位'
},
categoryLabel() { categoryLabel() {
const c = this.categories.find(x => String(x.id) === String(this.form.categoryId)) const c = this.categories.find(x => String(x.id) === String(this.form.categoryId))
return c ? c.name : '选择类别' return c ? c.name : '选择类别'
@@ -128,7 +97,7 @@ export default {
}, },
methods: { methods: {
async bootstrap() { async bootstrap() {
await Promise.all([this.fetchUnits(), this.fetchCategories()]) await Promise.all([this.fetchCategories()])
if (this.id) this.loadDetail() if (this.id) this.loadDetail()
}, },
initKeyboardListener() { initKeyboardListener() {
@@ -147,22 +116,12 @@ export default {
} }
} catch (_) {} } catch (_) {}
}, },
async fetchUnits() {
try {
const res = await get('/api/product-units')
this.units = Array.isArray(res?.list) ? res.list : (Array.isArray(res) ? res : [])
} catch (_) {}
},
async fetchCategories() { async fetchCategories() {
try { try {
const res = await get('/api/product-categories') const res = await get('/api/product-categories')
this.categories = Array.isArray(res?.list) ? res.list : (Array.isArray(res) ? res : []) this.categories = Array.isArray(res?.list) ? res.list : (Array.isArray(res) ? res : [])
} catch (_) {} } catch (_) {}
}, },
onPickUnit(e) {
const idx = Number(e.detail.value); const u = this.units[idx]
this.form.unitId = u ? u.id : ''
},
onPickCategory(e) { onPickCategory(e) {
const idx = Number(e.detail.value); const c = this.categories[idx] const idx = Number(e.detail.value); const c = this.categories[idx]
this.form.categoryId = c ? c.id : '' this.form.categoryId = c ? c.id : ''
@@ -193,12 +152,8 @@ export default {
const data = await get('/api/products/' + this.id) const data = await get('/api/products/' + this.id)
Object.assign(this.form, { Object.assign(this.form, {
name: data.name, name: data.name,
barcode: data.barcode, brand: data.brand, model: data.model, spec: data.spec, origin: data.origin, barcode: data.barcode, brand: data.brand, model: data.model, spec: data.spec,
categoryId: data.categoryId, unitId: data.unitId, categoryId: data.categoryId,
stock: data.stock,
safeMin: data.safeMin, safeMax: data.safeMax,
purchasePrice: data.purchasePrice, retailPrice: data.retailPrice,
wholesalePrice: data.wholesalePrice, bigClientPrice: data.bigClientPrice,
images: (data.images || []).map(i => i.url || i), images: (data.images || []).map(i => i.url || i),
remark: data.remark || '', remark: data.remark || '',
platformStatus: data.platformStatus || '', platformStatus: data.platformStatus || '',
@@ -208,21 +163,13 @@ export default {
}, },
validate() { validate() {
if (!this.form.name) { uni.showToast({ title: '请填写名称', icon: 'none' }); return false } if (!this.form.name) { uni.showToast({ title: '请填写名称', icon: 'none' }); return false }
if (this.form.safeMin != null && this.form.safeMax != null && Number(this.form.safeMin) > Number(this.form.safeMax)) {
uni.showToast({ title: '安全库存区间不合法', icon: 'none' }); return false
}
return true return true
}, },
buildPayload() { buildPayload() {
const f = this.form const f = this.form
return { return {
name: f.name, barcode: f.barcode, brand: f.brand, model: f.model, spec: f.spec, origin: f.origin, name: f.name, barcode: f.barcode, brand: f.brand, model: f.model, spec: f.spec,
categoryId: f.categoryId || null, unitId: f.unitId, categoryId: f.categoryId || null,
safeMin: f.safeMin, safeMax: f.safeMax,
prices: {
purchasePrice: f.purchasePrice, retailPrice: f.retailPrice, wholesalePrice: f.wholesalePrice, bigClientPrice: f.bigClientPrice
},
stock: f.stock,
images: f.images, images: f.images,
remark: f.remark remark: f.remark
} }
@@ -236,7 +183,7 @@ export default {
else await post('/api/products', payload) else await post('/api/products', payload)
uni.showToast({ title: '保存成功', icon: 'success', mask: false }) uni.showToast({ title: '保存成功', icon: 'success', mask: false })
if (goOn && !this.id) { if (goOn && !this.id) {
this.form = { name: '', barcode: '', brand: '', model: '', spec: '', origin: '', categoryId: '', unitId: '', stock: null, safeMin: null, safeMax: null, purchasePrice: null, retailPrice: null, wholesalePrice: null, bigClientPrice: null, images: [], remark: '', platformStatus: '', sourceSubmissionId: '' } this.form = { name: '', barcode: '', brand: '', model: '', spec: '', categoryId: '', images: [], remark: '', platformStatus: '', sourceSubmissionId: '' }
} else { } else {
setTimeout(() => uni.navigateBack(), 400) setTimeout(() => uni.navigateBack(), 400)
} }

View File

@@ -2,32 +2,59 @@
<view class="page"> <view class="page">
<view class="tabs"> <view class="tabs">
<view class="tab" :class="{active: tab==='all'}" @click="switchTab('all')">全部</view> <view class="tab" :class="{active: tab==='all'}" @click="switchTab('all')">全部</view>
<view class="tab" :class="{active: tab==='category'}" @click="switchTab('category')">按类别</view> <view class="tab" :class="{active: tab==='search'}" @click="switchTab('search')">查询</view>
<view class="tab extra" @click="goMySubmissions">我的提交</view> <view class="tab extra" @click="goMySubmissions">我的提交</view>
</view> </view>
<view class="search"> <view class="search" :class="{ 'template-mode': query.mode==='template' }" v-if="tab==='search'">
<input v-model.trim="query.kw" placeholder="输入名称/条码/规格查询" @confirm="reload" /> <view class="mode">
<picker mode="selector" :range="categoryNames" v-if="tab==='category'" @change="onPickCategory"> <picker mode="selector" :range="['直接查询','名称模糊查询','按模板参数查询']" @change="e => (query.mode = ['direct','nameLike','template'][Number(e.detail.value)] || 'direct')">
<view class="picker">{{ categoryLabel }}</view> <view class="picker">{{ modeLabel }}</view>
</picker> </picker>
</view>
<block v-if="query.mode==='direct' || query.mode==='nameLike'">
<input v-model.trim="query.kw" placeholder="输入名称/条码/规格查询" @confirm="reload" />
</block>
<block v-if="query.mode==='template'">
<view class="picker-row">
<picker mode="selector" :range="categoryNames" @change="onPickCategory">
<view class="picker">{{ categoryLabel }}</view>
</picker>
<picker mode="selector" :range="templateNames" @change="onPickTemplate">
<view class="picker">{{ templateLabel }}</view>
</picker>
</view>
<view class="params-wrap">
<view class="param-row" v-for="(p,idx) in selectedTemplateParams" :key="p.fieldKey">
<input v-if="p.type==='string'" v-model.trim="paramValues[p.fieldKey]" :placeholder="'输入' + p.fieldLabel" />
<input v-else-if="p.type==='number'" type="number" v-model.number="paramValues[p.fieldKey]" :placeholder="'输入' + p.fieldLabel" />
<switch v-else-if="p.type==='boolean'" :checked="!!paramValues[p.fieldKey]" @change="onParamBoolChange(p, $event)" />
<picker v-else-if="p.type==='enum'" mode="selector" :range="p.enumOptions||[]" @change="onPickParamEnumWrapper(p, $event)">
<view class="picker">{{ displayParamEnum(p) }}</view>
</picker>
<picker v-else-if="p.type==='date'" mode="date" @change="onParamDateChange(p, $event)">
<view class="picker">{{ paramValues[p.fieldKey] || ('选择' + p.fieldLabel) }}</view>
</picker>
<input v-else v-model.trim="paramValues[p.fieldKey]" :placeholder="'输入' + p.fieldLabel" />
</view>
</view>
</block>
<button size="mini" @click="reload">查询</button> <button size="mini" @click="reload">查询</button>
</view> </view>
<scroll-view scroll-y class="list" @scrolltolower="loadMore"> <scroll-view scroll-y class="list" @scrolltolower="loadMore">
<block v-if="items.length"> <block v-if="items.length">
<view class="item" v-for="it in items" :key="it.id" @click="openForm(it.id)"> <view class="item" v-for="it in items" :key="it.id" @click="openDetail(it.id)">
<image v-if="it.cover" :src="it.cover" class="thumb" mode="aspectFill" /> <image v-if="it.cover" :src="it.cover" class="thumb" mode="aspectFill" />
<view class="content"> <view class="content">
<view class="name"> <view class="name">
<text>{{ it.name }}</text> <text>{{ it.name }}</text>
<text v-if="it.deleted" class="tag-deleted">已删除</text>
<text v-if="it.platformStatus==='platform'" class="tag-platform">平台推荐</text> <text v-if="it.platformStatus==='platform'" class="tag-platform">平台推荐</text>
<text v-else-if="it.sourceSubmissionId" class="tag-custom">我的提交</text> <text v-else-if="it.sourceSubmissionId" class="tag-custom">我的提交</text>
</view> </view>
<view class="meta">{{ it.brand || '-' }} {{ it.model || '' }} {{ it.spec || '' }}</view> <view class="meta">{{ it.brand || '-' }} {{ it.model || '' }} {{ it.spec || '' }}</view>
<view class="meta">库存{{ it.stock ?? 0 }} <!-- 隐藏库存与价格展示按需求仅展示基础信息 -->
<text class="price">零售价¥{{ (it.retailPrice ?? it.price ?? 0).toFixed(2) }}</text>
</view>
</view> </view>
</view> </view>
</block> </block>
@@ -47,11 +74,13 @@ export default {
data() { data() {
return { return {
items: [], items: [],
query: { kw: '', page: 1, size: 20, categoryId: '' }, query: { kw: '', page: 1, size: 20, categoryId: '', mode: 'direct', templateId: '', params: {} },
finished: false, finished: false,
loading: false, loading: false,
tab: 'all', tab: 'all',
categories: [] categories: [],
templates: [],
paramValues: {}
} }
}, },
onLoad() { onLoad() {
@@ -76,26 +105,59 @@ export default {
categoryLabel() { categoryLabel() {
const c = this.categories.find(x => String(x.id) === String(this.query.categoryId)) const c = this.categories.find(x => String(x.id) === String(this.query.categoryId))
return c ? '类别:' + c.name : '选择类别' return c ? '类别:' + c.name : '选择类别'
} },
modeLabel() {
const map = { direct: '直接查询', nameLike: '名称模糊查询', template: '按模板参数查询' }
return map[this.query.mode] || '直接查询'
},
templateNames() { return this.templates.map(t => t.name) },
templateLabel() {
const t = this.templates.find(x => String(x.id) === String(this.query.templateId))
return t ? '模板:' + t.name : '选择模板'
},
selectedTemplate() { return this.templates.find(t => String(t.id) === String(this.query.templateId)) || null },
selectedTemplateParams() { return (this.selectedTemplate && Array.isArray(this.selectedTemplate.params)) ? this.selectedTemplate.params : [] }
}, },
methods: { methods: {
switchTab(t) { switchTab(t) {
this.tab = t this.tab = t
this.query.categoryId = '' this.query.categoryId = ''
this.query.templateId = ''
this.paramValues = {}
this.reload() this.reload()
}, },
onPickCategory(e) { onPickCategory(e) {
const idx = Number(e.detail.value) const idx = Number(e.detail.value)
const c = this.categories[idx] const c = this.categories[idx]
this.query.categoryId = c ? c.id : '' this.query.categoryId = c ? c.id : ''
this.reload() this.fetchTemplates()
}, },
onPickTemplate(e) {
const idx = Number(e.detail.value)
const t = this.templates[idx]
this.query.templateId = t ? t.id : ''
this.paramValues = {}
},
onPickParamEnumWrapper(p, e) {
const idx = Number(e.detail.value)
const arr = p.enumOptions || []
this.paramValues[p.fieldKey] = arr[idx]
},
onParamBoolChange(p, e) { this.paramValues[p.fieldKey] = e?.detail?.value ? true : false },
onParamDateChange(p, e) { this.paramValues[p.fieldKey] = e?.detail?.value || '' },
async fetchCategories() { async fetchCategories() {
try { try {
const res = await get('/api/product-categories', {}) const res = await get('/api/product-categories', {})
this.categories = Array.isArray(res?.list) ? res.list : (Array.isArray(res) ? res : []) this.categories = Array.isArray(res?.list) ? res.list : (Array.isArray(res) ? res : [])
} catch (_) {} } catch (_) {}
}, },
async fetchTemplates() {
try {
const res = await get('/api/product-templates', this.query.categoryId ? { categoryId: this.query.categoryId } : {})
const list = Array.isArray(res?.list) ? res.list : (Array.isArray(res) ? res : [])
this.templates = list
} catch (_) { this.templates = [] }
},
reload() { reload() {
this.items = [] this.items = []
this.query.page = 1 this.query.page = 1
@@ -107,7 +169,16 @@ export default {
this.loading = true this.loading = true
try { try {
const params = { kw: this.query.kw, page: this.query.page, size: this.query.size } const params = { kw: this.query.kw, page: this.query.page, size: this.query.size }
if (this.tab === 'category' && this.query.categoryId) params.categoryId = this.query.categoryId if (this.tab === 'search') {
if (this.query.categoryId) params.categoryId = this.query.categoryId
if (this.query.templateId) params.templateId = this.query.templateId
if (this.paramValues && Object.keys(this.paramValues).length) {
for (const k of Object.keys(this.paramValues)) {
const v = this.paramValues[k]
if (v !== undefined && v !== null && v !== '') params['param_' + k] = v
}
}
}
const res = await get('/api/products', params) const res = await get('/api/products', params)
const list = Array.isArray(res?.list) ? res.list : (Array.isArray(res) ? res : []) const list = Array.isArray(res?.list) ? res.list : (Array.isArray(res) ? res : [])
this.items = this.items.concat(list) this.items = this.items.concat(list)
@@ -119,13 +190,26 @@ export default {
this.loading = false this.loading = false
} }
}, },
openForm(id) { openDetail(id) {
const url = '/pages/product/form' + (id ? ('?id=' + id) : '') uni.navigateTo({ url: '/pages/product/product-detail?id=' + id })
uni.navigateTo({ url }) },
}, goMySubmissions() {
goMySubmissions() {
uni.navigateTo({ url: '/pages/product/submissions' }) uni.navigateTo({ url: '/pages/product/submissions' })
} },
async remove(it) {
try {
const r = await new Promise(resolve => {
uni.showModal({ content: '确认删除该货品?删除后可在后台恢复', success: resolve })
})
if (!r || !r.confirm) return
const { del } = require('../../common/http.js')
await del('/api/products/' + it.id)
uni.showToast({ title: '已删除', icon: 'success' })
this.reload()
} catch (e) {
uni.showToast({ title: '删除失败', icon: 'none' })
}
}
} }
} }
</script> </script>
@@ -139,6 +223,9 @@ export default {
.search { display:flex; gap: 12rpx; padding: 16rpx; background:$uni-bg-color-grey; align-items: center; } .search { display:flex; gap: 12rpx; padding: 16rpx; background:$uni-bg-color-grey; align-items: center; }
.search input { flex:1; background:$uni-bg-color-hover; border-radius: 12rpx; padding: 12rpx; color: $uni-text-color; } .search input { flex:1; background:$uni-bg-color-hover; border-radius: 12rpx; padding: 12rpx; color: $uni-text-color; }
.picker { padding: 8rpx 12rpx; background:$uni-bg-color-hover; border-radius: 10rpx; color:$uni-text-color-grey; } .picker { padding: 8rpx 12rpx; background:$uni-bg-color-hover; border-radius: 10rpx; color:$uni-text-color-grey; }
.template-mode { flex-direction: column; align-items: stretch; gap: 8rpx; }
.picker-row { display:flex; gap: 12rpx; }
.params-wrap { margin-top: 6rpx; background:$uni-bg-color-grey; border-radius: 12rpx; padding: 8rpx 8rpx; }
.list { flex:1; } .list { flex:1; }
.item { display:flex; padding: 20rpx; background:$uni-bg-color-grey; border-bottom: 1rpx solid $uni-border-color; } .item { display:flex; padding: 20rpx; background:$uni-bg-color-grey; border-bottom: 1rpx solid $uni-border-color; }
.thumb { width: 120rpx; height: 120rpx; border-radius: 12rpx; margin-right: 16rpx; background:$uni-bg-color-hover; } .thumb { width: 120rpx; height: 120rpx; border-radius: 12rpx; margin-right: 16rpx; background:$uni-bg-color-hover; }
@@ -146,6 +233,7 @@ export default {
.name { color:$uni-text-color; margin-bottom: 6rpx; font-weight: 600; display:flex; align-items:center; gap: 12rpx; } .name { color:$uni-text-color; margin-bottom: 6rpx; font-weight: 600; display:flex; align-items:center; gap: 12rpx; }
.tag-platform { font-size: 22rpx; color:#fff; background:#2d8cf0; padding: 4rpx 10rpx; border-radius: 8rpx; } .tag-platform { font-size: 22rpx; color:#fff; background:#2d8cf0; padding: 4rpx 10rpx; border-radius: 8rpx; }
.tag-custom { font-size: 22rpx; color:#fff; background:#67c23a; padding: 4rpx 10rpx; border-radius: 8rpx; } .tag-custom { font-size: 22rpx; color:#fff; background:#67c23a; padding: 4rpx 10rpx; border-radius: 8rpx; }
.tag-deleted { font-size: 22rpx; color:#fff; background:#909399; padding: 4rpx 10rpx; border-radius: 8rpx; }
.meta { color:$uni-text-color-grey; font-size: 24rpx; } .meta { color:$uni-text-color-grey; font-size: 24rpx; }
.price { margin-left: 20rpx; color:$uni-color-primary; } .price { margin-left: 20rpx; color:$uni-color-primary; }
.empty { height: 60vh; display:flex; align-items:center; justify-content:center; color:$uni-text-color-grey; } .empty { height: 60vh; display:flex; align-items:center; justify-content:center; color:$uni-text-color-grey; }

View File

@@ -0,0 +1,142 @@
<template>
<scroll-view scroll-y class="page" v-if="detail">
<view class="header">
<text class="model">{{ detail.model }}</text>
<text v-if="detail.deleted" class="status deleted">已删除</text>
</view>
<view class="section">
<view class="row"><text class="label">名称</text><text class="value">{{ detail.name || '-' }}</text></view>
<view class="row"><text class="label">品牌</text><text class="value">{{ detail.brand || '-' }}</text></view>
<view class="row"><text class="label">型号</text><text class="value">{{ detail.model || '-' }}</text></view>
<view class="row"><text class="label">条码</text><text class="value">{{ detail.barcode || '-' }}</text></view>
<view class="row"><text class="label">类别</text><text class="value">{{ categoryName }}</text></view>
<view class="row"><text class="label">模板</text><text class="value">{{ templateName }}</text></view>
<view class="row" v-if="detail.externalCode"><text class="label">编号</text><text class="value">{{ detail.externalCode }}</text></view>
</view>
<view class="section">
<view class="block-title">参数</view>
<view v-if="labeledPairs.length" class="params">
<view class="param" v-for="item in labeledPairs" :key="item.key">
<text class="param-key">{{ item.label }}<text v-if="item.unit">{{ item.unit }}</text></text>
<text class="param-val">{{ item.value }}</text>
</view>
</view>
<view v-else class="placeholder">未填写参数</view>
</view>
<view class="section">
<view class="block-title">图片</view>
<view v-if="detail.images && detail.images.length" class="images">
<image v-for="(img, idx) in detail.images" :key="idx" :src="img.url || img" class="image" mode="aspectFill" @click="preview(idx)" />
</view>
<view v-else class="placeholder">未上传图片</view>
</view>
<view class="section">
<view class="block-title">备注</view>
<view class="placeholder">{{ detail.remark || '无' }}</view>
</view>
<view class="footer">
<button size="mini" @click="back">返回</button>
<button size="mini" type="warn" @click="remove">删除</button>
</view>
</scroll-view>
<view v-else class="loading">加载中...</view>
</template>
<script>
import { get, del } from '../../common/http.js'
export default {
data() {
return { id: '', detail: null, categoryName: '-', templateName: '-' }
},
async onLoad(query) {
this.id = query?.id || ''
if (!this.id) { uni.showToast({ title: '参数缺失', icon: 'none' }); return }
await this.preloadDictionaries()
await this.loadDetail()
},
methods: {
async preloadDictionaries() {
try {
const needCats = !Array.isArray(uni.getStorageSync('CACHE_CATEGORIES'))
const needTpls = !Array.isArray(uni.getStorageSync('CACHE_TEMPLATES'))
if (!needCats && !needTpls) return
const reqs = []
if (needCats) reqs.push(get('/api/product-categories'))
if (needTpls) reqs.push(get('/api/product-templates'))
const res = await Promise.all(reqs)
let idx = 0
if (needCats) { const r = res[idx++]; const list = Array.isArray(r?.list)?r.list:(Array.isArray(r)?r:[]); uni.setStorageSync('CACHE_CATEGORIES', list) }
if (needTpls) { const r = res[idx++]; const list = Array.isArray(r?.list)?r.list:(Array.isArray(r)?r:[]); uni.setStorageSync('CACHE_TEMPLATES', list) }
} catch (_) {}
},
async loadDetail() {
try {
const data = await get('/api/products/' + this.id)
this.detail = data
this.categoryName = this.categoryLookup(data.categoryId)
this.templateName = this.templateLookup(data.templateId)
} catch (e) { uni.showToast({ title: e?.message || '加载失败', icon: 'none' }) }
},
preview(idx) {
try { const list = (this.detail?.images||[]).map(i => i.url || i); uni.previewImage({ urls: list, current: idx }) } catch (_) {}
},
categoryLookup(id) {
try { const list = uni.getStorageSync('CACHE_CATEGORIES') || []; const f = list.find(x => String(x.id)===String(id)); return f?f.name:'-'} catch(_){return'-'}
},
templateLookup(id) {
try { const list = uni.getStorageSync('CACHE_TEMPLATES') || []; const f = list.find(x => String(x.id)===String(id)); return f?f.name:'-'} catch(_){return'-'}
},
async remove() {
try {
const r = await new Promise(resolve => { uni.showModal({ content: '确认删除该货品?删除后可在后台恢复', success: resolve }) })
if (!r || !r.confirm) return
await del('/api/products/' + this.id)
uni.showToast({ title: '已删除', icon: 'success' })
setTimeout(() => uni.navigateBack(), 400)
} catch (e) { uni.showToast({ title: '删除失败', icon: 'none' }) }
},
back(){ uni.navigateBack({ delta: 1 }) }
},
computed: {
labeledPairs() {
const params = this.detail?.parameters
if (!params || typeof params !== 'object') return []
let labelMap = {}, unitMap = {}
try {
const templates = uni.getStorageSync('CACHE_TEMPLATES') || []
const tpl = templates.find(t => String(t.id) === String(this.detail?.templateId))
if (tpl && Array.isArray(tpl.params)) for (const p of tpl.params) { labelMap[p.fieldKey] = p.fieldLabel; unitMap[p.fieldKey] = p.unit }
} catch (_) {}
return Object.keys(params).map(k => ({ key: k, label: labelMap[k] || k, unit: unitMap[k] || '', value: params[k] }))
}
}
}
</script>
<style lang="scss">
.page { padding: 24rpx 24rpx 160rpx; background: #f6f7fb; }
.header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20rpx; }
.model { font-size: 36rpx; font-weight: 700; color: #2d3a4a; }
.status.deleted { font-size: 24rpx; padding: 6rpx 18rpx; border-radius: 999rpx; background: #c0c4cc; color: #fff; }
.section { background: #fff; border-radius: 16rpx; padding: 20rpx 22rpx; margin-bottom: 24rpx; box-shadow: 0 8rpx 24rpx rgba(0,0,0,0.04); }
.row { display: flex; justify-content: space-between; padding: 12rpx 0; border-bottom: 1rpx solid #f1f2f5; }
.row:last-child { border-bottom: none; }
.label { width: 160rpx; font-size: 26rpx; color: #7a8899; }
.value { flex: 1; text-align: right; font-size: 26rpx; color: #2d3a4a; word-break: break-all; }
.block-title { font-size: 28rpx; font-weight: 600; color: #2d3a4a; margin-bottom: 12rpx; }
.placeholder { font-size: 26rpx; color: #7a8899; }
.params { display: flex; flex-direction: column; gap: 12rpx; }
.param { display: flex; justify-content: space-between; font-size: 26rpx; color: #2d3a4a; }
.param-key { color: #7a8899; }
.images { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12rpx; }
.image { width: 100%; height: 200rpx; border-radius: 16rpx; background: #f0f2f5; }
.footer { display: flex; justify-content: flex-end; gap: 20rpx; }
.loading { height: 100vh; display: flex; align-items: center; justify-content: center; color: #7a8899; }
</style>

View File

@@ -9,18 +9,17 @@
<view class="row"><text class="label">名称</text><text class="value">{{ detail.name || '-' }}</text></view> <view class="row"><text class="label">名称</text><text class="value">{{ detail.name || '-' }}</text></view>
<view class="row"><text class="label">品牌</text><text class="value">{{ detail.brand || '-' }}</text></view> <view class="row"><text class="label">品牌</text><text class="value">{{ detail.brand || '-' }}</text></view>
<view class="row"><text class="label">规格</text><text class="value">{{ detail.spec || '-' }}</text></view> <view class="row"><text class="label">规格</text><text class="value">{{ detail.spec || '-' }}</text></view>
<view class="row"><text class="label">产地</text><text class="value">{{ detail.origin || '-' }}</text></view>
<view class="row"><text class="label">条码</text><text class="value">{{ detail.barcode || '-' }}</text></view> <view class="row"><text class="label">条码</text><text class="value">{{ detail.barcode || '-' }}</text></view>
<view class="row"><text class="label">单位</text><text class="value">{{ unitName }}</text></view>
<view class="row"><text class="label">类别</text><text class="value">{{ categoryName }}</text></view> <view class="row"><text class="label">类别</text><text class="value">{{ categoryName }}</text></view>
<view class="row"><text class="label">安全库存</text><text class="value">{{ stockRange }}</text></view> <view class="row"><text class="label">模板</text><text class="value">{{ templateName }}</text></view>
<!-- 隐藏产地/单位/安全库存显示 -->
</view> </view>
<view class="section"> <view class="section">
<view class="block-title">参数</view> <view class="block-title">参数</view>
<view v-if="parameterPairs.length" class="params"> <view v-if="labeledPairs.length" class="params">
<view class="param" v-for="item in parameterPairs" :key="item.key"> <view class="param" v-for="item in labeledPairs" :key="item.key">
<text class="param-key">{{ item.key }}</text> <text class="param-key">{{ item.label }}</text>
<text class="param-val">{{ item.value }}</text> <text class="param-val">{{ item.value }}</text>
</view> </view>
</view> </view>
@@ -63,7 +62,8 @@ export default {
id: '', id: '',
detail: null, detail: null,
unitName: '-', unitName: '-',
categoryName: '-' categoryName: '-',
templateName: '-'
} }
}, },
async onLoad(query) { async onLoad(query) {
@@ -79,8 +79,9 @@ export default {
try { try {
const data = await get(`/api/products/submissions/${this.id}`) const data = await get(`/api/products/submissions/${this.id}`)
this.detail = data this.detail = data
this.unitName = this.unitLookup(data.unitId) // 单位已移除
this.categoryName = this.categoryLookup(data.categoryId) this.categoryName = this.categoryLookup(data.categoryId)
this.templateName = this.templateLookup(data.templateId)
} catch (e) { } catch (e) {
const msg = e?.message || '加载失败' const msg = e?.message || '加载失败'
uni.showToast({ title: msg, icon: 'none' }) uni.showToast({ title: msg, icon: 'none' })
@@ -96,11 +97,7 @@ export default {
if (s === 'rejected') return 'rejected' if (s === 'rejected') return 'rejected'
return 'pending' return 'pending'
}, },
parameterPairs() {
const params = this.detail?.parameters
if (!params || typeof params !== 'object') return []
return Object.keys(params).map(k => ({ key: k, value: params[k] }))
},
preview(idx) { preview(idx) {
if (!this.detail?.images || !this.detail.images.length) return if (!this.detail?.images || !this.detail.images.length) return
uni.previewImage({ urls: this.detail.images, current: idx }) uni.previewImage({ urls: this.detail.images, current: idx })
@@ -120,7 +117,7 @@ export default {
}, },
unitLookup(id) { unitLookup(id) {
try { try {
const list = uni.getStorageSync('CACHE_UNITS') || [] const list = []
const found = list.find(x => String(x.id) === String(id)) const found = list.find(x => String(x.id) === String(id))
return found ? found.name : '-' return found ? found.name : '-'
} catch (_) { return '-' } } catch (_) { return '-' }
@@ -132,6 +129,13 @@ export default {
return found ? found.name : '-' return found ? found.name : '-'
} catch (_) { return '-' } } catch (_) { return '-' }
}, },
templateLookup(id) {
try {
const list = uni.getStorageSync('CACHE_TEMPLATES') || []
const found = list.find(x => String(x.id) === String(id))
return found ? found.name : '-'
} catch (_) { return '-' }
},
back() { back() {
uni.navigateBack({ delta: 1 }) uni.navigateBack({ delta: 1 })
}, },
@@ -160,6 +164,20 @@ export default {
if (min != null && max != null) return `${min} ~ ${max}` if (min != null && max != null) return `${min} ~ ${max}`
if (min != null) return `${min}` if (min != null) return `${min}`
return `${max}` return `${max}`
},
labeledPairs() {
const params = this.detail?.parameters
if (!params || typeof params !== 'object') return []
// 从缓存模板中读取 label
let labelMap = {}
try {
const templates = uni.getStorageSync('CACHE_TEMPLATES') || []
const tpl = templates.find(t => String(t.id) === String(this.detail?.templateId))
if (tpl && Array.isArray(tpl.params)) {
for (const p of tpl.params) labelMap[p.fieldKey] = p.fieldLabel
}
} catch (_) {}
return Object.keys(params).map(k => ({ key: k, label: labelMap[k] || k, value: params[k] }))
} }
} }
} }

View File

@@ -68,9 +68,10 @@ export default {
methods: { methods: {
async preloadDictionaries() { async preloadDictionaries() {
try { try {
const [units, categories] = await Promise.all([ const [units, categories, templates] = await Promise.all([
this.cacheUnitsLoaded ? Promise.resolve(null) : get('/api/product-units'), this.cacheUnitsLoaded ? Promise.resolve(null) : get('/api/product-units'),
this.cacheCategoriesLoaded ? Promise.resolve(null) : get('/api/product-categories') this.cacheCategoriesLoaded ? Promise.resolve(null) : get('/api/product-categories'),
get('/api/product-templates')
]) ])
if (units) { if (units) {
const list = Array.isArray(units?.list) ? units.list : (Array.isArray(units) ? units : []) const list = Array.isArray(units?.list) ? units.list : (Array.isArray(units) ? units : [])
@@ -82,6 +83,10 @@ export default {
uni.setStorageSync('CACHE_CATEGORIES', list) uni.setStorageSync('CACHE_CATEGORIES', list)
this.cacheCategoriesLoaded = true this.cacheCategoriesLoaded = true
} }
if (templates) {
const list = Array.isArray(templates?.list) ? templates.list : (Array.isArray(templates) ? templates : [])
uni.setStorageSync('CACHE_TEMPLATES', list)
}
} catch (_) { } catch (_) {
// 忽略缓存失败 // 忽略缓存失败
} }

View File

@@ -32,6 +32,13 @@
</view> </view>
</view> </view>
<view class="section">
<view class="row">
<text class="label">编号</text>
<input v-model.trim="form.externalCode" placeholder="内部/外部编号(可选)" />
</view>
</view>
<view class="section"> <view class="section">
<view class="row"> <view class="row">
<text class="label">模板</text> <text class="label">模板</text>
@@ -43,6 +50,42 @@
</view> </view>
</view> </view>
<!-- 动态参数根据模板渲染必填/可选项 -->
<view class="section" v-if="selectedTemplate">
<view class="row">
<text class="label">参数</text>
</view>
<view class="param-list">
<view class="row" v-for="(p,idx) in (selectedTemplate.params||[])" :key="p.fieldKey">
<text class="label">
{{ p.fieldLabel }}<text v-if="p.unit">{{ p.unit }}</text><text v-if="p.required" style="color:#ff5b5b">*</text>
</text>
<block v-if="p.type==='string'">
<input v-model.trim="paramValues[p.fieldKey]" :placeholder="'请输入' + p.fieldLabel" />
</block>
<block v-else-if="p.type==='number'">
<input type="number" v-model.number="paramValues[p.fieldKey]" :placeholder="'请输入' + p.fieldLabel" />
</block>
<block v-else-if="p.type==='boolean'">
<switch :checked="!!paramValues[p.fieldKey]" @change="e => (paramValues[p.fieldKey]=e.detail.value)" />
</block>
<block v-else-if="p.type==='enum'">
<picker mode="selector" :range="p.enumOptions||[]" @change="onPickEnum(p, $event)">
<view class="picker">{{ displayEnum(p) }}</view>
</picker>
</block>
<block v-else-if="p.type==='date'">
<picker mode="date" @change="onPickDate(p, $event)">
<view class="picker">{{ paramValues[p.fieldKey] || ('选择' + p.fieldLabel) }}</view>
</picker>
</block>
<block v-else>
<input v-model.trim="paramValues[p.fieldKey]" :placeholder="'请输入' + p.fieldLabel" />
</block>
</view>
</view>
</view>
<view class="section"> <view class="section">
<view class="row"> <view class="row">
<text class="label">图片</text> <text class="label">图片</text>
@@ -91,6 +134,7 @@ export default {
categoryId: '', categoryId: '',
templateId: '', templateId: '',
externalCode: '',
parameters: {}, parameters: {},
images: [], images: [],
remark: '', remark: '',
@@ -172,11 +216,18 @@ export default {
this.form.templateId = t ? t.id : '' this.form.templateId = t ? t.id : ''
this.paramValues = {} this.paramValues = {}
}, },
onPickDate(p, e) {
this.paramValues[p.fieldKey] = e?.detail?.value || ''
},
onPickEnum(p, e) { onPickEnum(p, e) {
const idx = Number(e.detail.value) const idx = Number(e.detail.value)
const arr = p.enumOptions || [] const arr = p.enumOptions || []
this.paramValues[p.fieldKey] = arr[idx] this.paramValues[p.fieldKey] = arr[idx]
}, },
displayEnum(p) {
const v = this.paramValues[p.fieldKey]
return (v === undefined || v === null || v === '') ? ('选择' + p.fieldLabel) : String(v)
},
async scanBarcode() { async scanBarcode() {
try { try {
const chooseRes = await uni.chooseImage({ count: 1, sourceType: ['camera','album'], sizeType: ['compressed'] }) const chooseRes = await uni.chooseImage({ count: 1, sourceType: ['camera','album'], sizeType: ['compressed'] })
@@ -253,6 +304,7 @@ export default {
model: this.form.model, model: this.form.model,
brand: this.form.brand, brand: this.form.brand,
barcode: this.form.barcode, barcode: this.form.barcode,
externalCode: this.form.externalCode || null,
categoryId: this.form.categoryId || null, categoryId: this.form.categoryId || null,
templateId: this.form.templateId || null, templateId: this.form.templateId || null,
parameters: paramsForSubmit, parameters: paramsForSubmit,

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

Binary file not shown.

View File

@@ -74,3 +74,9 @@ $uni-color-subtitle: #555555; // 二级标题颜色
$uni-font-size-subtitle:26px; $uni-font-size-subtitle:26px;
$uni-color-paragraph: #3F536E; // 文章段落颜色 $uni-color-paragraph: #3F536E; // 文章段落颜色
$uni-font-size-paragraph:15px; $uni-font-size-paragraph:15px;
/* 表单控件尺寸(统一配置,避免页面内硬编码) */
$app-form-control-height: 76rpx; // 控件总高度(含边框)
$app-form-control-padding-x: 20rpx; // 水平方向内边距
$app-form-control-border-width: 2rpx; // 边框宽度
$app-form-control-border-radius: 12rpx; // 圆角半径

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
{"version":3,"file":"constants.js","sources":["common/constants.js"],"sourcesContent":["// 统一常量配置:其他收入/支出分类,禁止在业务中硬编码\r\nexport const INCOME_CATEGORIES = [\r\n\t{ key: 'sale_income', label: '销售收入' },\r\n\t{ key: 'operation_income', label: '经营所得' },\r\n\t{ key: 'interest_income', label: '利息收入' },\r\n\t{ key: 'investment_income', label: '投资收入' },\r\n\t{ key: 'other_income', label: '其它收入' }\r\n]\r\n\r\nexport const EXPENSE_CATEGORIES = [\r\n\t{ key: 'operation_expense', label: '经营支出' },\r\n\t{ key: 'office_supplies', label: '办公用品' },\r\n\t{ key: 'rent', label: '房租' },\r\n\t{ key: 'interest_expense', label: '利息支出' },\r\n\t{ key: 'other_expense', label: '其它支出' }\r\n]\r\n\r\n// 路由常量(集中管理页面路径,避免在业务中硬编码)\r\nexport const ROUTES = {\r\n\thome: '/pages/index/index',\r\n\tproductList: '/pages/product/list',\r\n\tproductForm: '/pages/product/form',\r\n\tproductSelect: '/pages/product/select',\r\n\tproductSettings: '/pages/product/settings',\r\n\torderCreate: '/pages/order/create',\r\n\tdetail: '/pages/detail/index',\r\n\tmy: '/pages/my/index',\r\n\tmyAbout: '/pages/my/about',\r\n\tmyVip: '/pages/my/vip',\r\n\treport: '/pages/report/index',\r\n\tcustomerSelect: '/pages/customer/select',\r\n\tsupplierSelect: '/pages/supplier/select',\r\n\taccountSelect: '/pages/account/select'\r\n}\r\n\r\n\r\n// 本地存储键:统一管理,避免在业务中散落硬编码\r\nexport const STORAGE_KEYS = {\r\n\tVIP_IS_VIP: 'USER_VIP_IS_VIP',\r\n\tVIP_START: 'USER_VIP_START',\r\n\tVIP_END: 'USER_VIP_END'\r\n}\r\n\r\n\r\n"],"names":[],"mappings":";AACY,MAAC,oBAAoB;AAAA,EAChC,EAAE,KAAK,eAAe,OAAO,OAAQ;AAAA,EACrC,EAAE,KAAK,oBAAoB,OAAO,OAAQ;AAAA,EAC1C,EAAE,KAAK,mBAAmB,OAAO,OAAQ;AAAA,EACzC,EAAE,KAAK,qBAAqB,OAAO,OAAQ;AAAA,EAC3C,EAAE,KAAK,gBAAgB,OAAO,OAAQ;AACvC;AAEY,MAAC,qBAAqB;AAAA,EACjC,EAAE,KAAK,qBAAqB,OAAO,OAAQ;AAAA,EAC3C,EAAE,KAAK,mBAAmB,OAAO,OAAQ;AAAA,EACzC,EAAE,KAAK,QAAQ,OAAO,KAAM;AAAA,EAC5B,EAAE,KAAK,oBAAoB,OAAO,OAAQ;AAAA,EAC1C,EAAE,KAAK,iBAAiB,OAAO,OAAQ;AACxC;AAGY,MAAC,SAAS;AAAA,EACrB,MAAM;AAAA,EACN,aAAa;AAAA,EACb,aAAa;AAAA,EACb,eAAe;AAAA,EACf,iBAAiB;AAAA,EACjB,aAAa;AAAA,EACb,QAAQ;AAAA,EACR,IAAI;AAAA,EACJ,SAAS;AAAA,EACT,OAAO;AAAA,EACP,QAAQ;AAAA,EACR,gBAAgB;AAAA,EAChB,gBAAgB;AAAA,EAChB,eAAe;AAChB;;;;"} {"version":3,"file":"constants.js","sources":["common/constants.js"],"sourcesContent":["// 统一常量配置:其他收入/支出分类,禁止在业务中硬编码\nexport const INCOME_CATEGORIES = [\n\t{ key: 'sale_income', label: '销售收入' },\n\t{ key: 'operation_income', label: '经营所得' },\n\t{ key: 'interest_income', label: '利息收入' },\n\t{ key: 'investment_income', label: '投资收入' },\n\t{ key: 'other_income', label: '其它收入' }\n]\n\nexport const EXPENSE_CATEGORIES = [\n\t{ key: 'operation_expense', label: '经营支出' },\n\t{ key: 'office_supplies', label: '办公用品' },\n\t{ key: 'rent', label: '房租' },\n\t{ key: 'interest_expense', label: '利息支出' },\n\t{ key: 'other_expense', label: '其它支出' }\n]\n\n// 路由常量(集中管理页面路径,避免在业务中硬编码)\nexport const ROUTES = {\n\thome: '/pages/index/index',\n\tproductList: '/pages/product/list',\n\tproductForm: '/pages/product/form',\n\tproductSelect: '/pages/product/select',\n\tproductSettings: '/pages/product/settings',\n\torderCreate: '/pages/order/create',\n\tdetail: '/pages/detail/index',\n\tmy: '/pages/my/index',\n\tmyAbout: '/pages/my/about',\n\tmyVip: '/pages/my/vip',\n\treport: '/pages/report/index',\n\tcustomerSelect: '/pages/customer/select',\n\tsupplierSelect: '/pages/supplier/select',\n\taccountSelect: '/pages/account/select'\n}\n\n\n// 本地存储键:统一管理,避免在业务中散落硬编码\nexport const STORAGE_KEYS = {\n\tVIP_IS_VIP: 'USER_VIP_IS_VIP',\n\tVIP_START: 'USER_VIP_START',\n\tVIP_END: 'USER_VIP_END'\n}\n\n\n// KPI 标签常量,避免页面硬编码\nexport const KPI_LABELS = {\n todaySales: '今日销售额',\n monthSales: '本月销售额',\n monthProfit: '本月利润',\n stockCount: '库存量'\n}\n\n"],"names":[],"mappings":";AACY,MAAC,oBAAoB;AAAA,EAChC,EAAE,KAAK,eAAe,OAAO,OAAQ;AAAA,EACrC,EAAE,KAAK,oBAAoB,OAAO,OAAQ;AAAA,EAC1C,EAAE,KAAK,mBAAmB,OAAO,OAAQ;AAAA,EACzC,EAAE,KAAK,qBAAqB,OAAO,OAAQ;AAAA,EAC3C,EAAE,KAAK,gBAAgB,OAAO,OAAQ;AACvC;AAEY,MAAC,qBAAqB;AAAA,EACjC,EAAE,KAAK,qBAAqB,OAAO,OAAQ;AAAA,EAC3C,EAAE,KAAK,mBAAmB,OAAO,OAAQ;AAAA,EACzC,EAAE,KAAK,QAAQ,OAAO,KAAM;AAAA,EAC5B,EAAE,KAAK,oBAAoB,OAAO,OAAQ;AAAA,EAC1C,EAAE,KAAK,iBAAiB,OAAO,OAAQ;AACxC;AAGY,MAAC,SAAS;AAAA,EACrB,MAAM;AAAA,EACN,aAAa;AAAA,EACb,aAAa;AAAA,EACb,eAAe;AAAA,EACf,iBAAiB;AAAA,EACjB,aAAa;AAAA,EACb,QAAQ;AAAA,EACR,IAAI;AAAA,EACJ,SAAS;AAAA,EACT,OAAO;AAAA,EACP,QAAQ;AAAA,EACR,gBAAgB;AAAA,EAChB,gBAAgB;AAAA,EAChB,eAAe;AAChB;AAYY,MAAC,aAAa;AAAA,EACtB,YAAY;AAAA,EACZ,YAAY;AAAA,EACZ,aAAa;AAAA,EACb,YAAY;AAChB;;;;;"}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,8 +1,8 @@
;(function(){ ;(function(){
let u=void 0,isReady=false,onReadyCallbacks=[],isServiceReady=false,onServiceReadyCallbacks=[]; let u=void 0,isReady=false,onReadyCallbacks=[],isServiceReady=false,onServiceReadyCallbacks=[];
const __uniConfig = {"pages":[],"globalStyle":{"backgroundColor":"#F8F8F8","navigationBar":{"backgroundColor":"#F8F8F8","titleText":"五金配件管家","type":"default","titleColor":"#000000"},"isNVue":false},"nvue":{"compiler":"uni-app","styleCompiler":"uni-app","flex-direction":"column"},"renderer":"auto","appname":"林林林","splashscreen":{"alwaysShowBeforeRender":true,"autoclose":true},"compilerVersion":"4.76","entryPagePath":"pages/index/index","entryPageQuery":"","realEntryPagePath":"","networkTimeout":{"request":60000,"connectSocket":60000,"uploadFile":60000,"downloadFile":60000},"tabBar":{"position":"bottom","color":"#8a7535","selectedColor":"#B4880F","borderStyle":"black","blurEffect":"none","fontSize":"10px","iconWidth":"24px","spacing":"3px","height":"50px","list":[{"pagePath":"pages/index/index","text":"首页","iconPath":"/static/logo.png","selectedIconPath":"/static/logo.png"},{"pagePath":"pages/product/list","text":"货品","iconPath":"/static/logo.png","selectedIconPath":"/static/logo.png"},{"pagePath":"pages/order/create","text":"开单","iconPath":"/static/logo.png","selectedIconPath":"/static/logo.png"},{"pagePath":"pages/detail/index","text":"明细","iconPath":"/static/logo.png","selectedIconPath":"/static/logo.png"},{"pagePath":"pages/my/index","text":"我的","iconPath":"/static/logo.png","selectedIconPath":"/static/logo.png"}],"backgroundColor":"#ffffff","selectedIndex":0,"shown":true},"locales":{},"darkmode":false,"themeConfig":{}}; const __uniConfig = {"pages":[],"globalStyle":{"backgroundColor":"#FFFFFF","backgroundTextStyle":"dark","navigationBar":{"backgroundColor":"#FFFFFF","titleText":"五金配件管家","type":"default","titleColor":"#000000"},"isNVue":false},"nvue":{"compiler":"uni-app","styleCompiler":"uni-app","flex-direction":"column"},"renderer":"auto","appname":"林林林","splashscreen":{"alwaysShowBeforeRender":true,"autoclose":true},"compilerVersion":"4.76","entryPagePath":"pages/index/index","entryPageQuery":"","realEntryPagePath":"","networkTimeout":{"request":60000,"connectSocket":60000,"uploadFile":60000,"downloadFile":60000},"tabBar":{"position":"bottom","color":"#444444","selectedColor":"#4C8DFF","borderStyle":"black","blurEffect":"none","fontSize":"10px","iconWidth":"24px","spacing":"3px","height":"50px","list":[{"pagePath":"pages/index/index","text":"首页","iconPath":"/static/icons/home.png","selectedIconPath":"/static/icons/home.png"},{"pagePath":"pages/product/list","text":"货品","iconPath":"/static/icons/product.png","selectedIconPath":"/static/icons/product.png"},{"pagePath":"pages/order/create","text":"开单","iconPath":"/static/icons/icons8-purchase-order-100.png","selectedIconPath":"/static/icons/icons8-purchase-order-100.png"},{"pagePath":"pages/detail/index","text":"明细","iconPath":"/static/icons/icons8-more-details-100.png","selectedIconPath":"/static/icons/icons8-more-details-100.png"},{"pagePath":"pages/my/index","text":"我的","iconPath":"/static/icons/icons8-account-male-100.png","selectedIconPath":"/static/icons/icons8-account-male-100.png"}],"backgroundColor":"#FFFFFF","selectedIndex":0,"shown":true},"locales":{},"darkmode":false,"themeConfig":{}};
const __uniRoutes = [{"path":"pages/index/index","meta":{"isQuit":true,"isEntry":true,"isTabBar":true,"tabBarIndex":0,"navigationBar":{"titleText":"五金配件管家","type":"default"},"isNVue":false}},{"path":"pages/order/create","meta":{"isQuit":true,"isTabBar":true,"tabBarIndex":2,"navigationBar":{"titleText":"开单","type":"default"},"isNVue":false}},{"path":"pages/product/select","meta":{"navigationBar":{"titleText":"选择商品","type":"default"},"isNVue":false}},{"path":"pages/product/list","meta":{"isQuit":true,"isTabBar":true,"tabBarIndex":1,"navigationBar":{"titleText":"货品列表","type":"default"},"isNVue":false}},{"path":"pages/product/form","meta":{"navigationBar":{"titleText":"编辑货品","type":"default"},"isNVue":false}},{"path":"pages/product/categories","meta":{"navigationBar":{"titleText":"类别管理","type":"default"},"isNVue":false}},{"path":"pages/product/units","meta":{"navigationBar":{"titleText":"单位管理","type":"default"},"isNVue":false}},{"path":"pages/product/settings","meta":{"navigationBar":{"titleText":"货品设置","type":"default"},"isNVue":false}},{"path":"pages/customer/select","meta":{"navigationBar":{"titleText":"选择客户","type":"default"},"isNVue":false}},{"path":"pages/customer/form","meta":{"navigationBar":{"titleText":"新增客户","type":"default"},"isNVue":false}},{"path":"pages/customer/detail","meta":{"navigationBar":{"titleText":"客户详情","type":"default"},"isNVue":false}},{"path":"pages/supplier/select","meta":{"navigationBar":{"titleText":"选择供应商","type":"default"},"isNVue":false}},{"path":"pages/supplier/form","meta":{"navigationBar":{"titleText":"新增/编辑供应商","type":"default"},"isNVue":false}},{"path":"pages/account/select","meta":{"navigationBar":{"titleText":"选择账户","type":"default"},"isNVue":false}},{"path":"pages/account/ledger","meta":{"navigationBar":{"titleText":"账户流水","type":"default"},"isNVue":false}},{"path":"pages/account/form","meta":{"navigationBar":{"titleText":"新增/编辑账户","type":"default"},"isNVue":false}},{"path":"pages/detail/index","meta":{"isQuit":true,"isTabBar":true,"tabBarIndex":3,"navigationBar":{"titleText":"明细","type":"default"},"isNVue":false}},{"path":"pages/my/index","meta":{"isQuit":true,"isTabBar":true,"tabBarIndex":4,"navigationBar":{"titleText":"我的","type":"default"},"isNVue":false}},{"path":"pages/my/about","meta":{"navigationBar":{"titleText":"关于与协议","type":"default"},"isNVue":false}},{"path":"pages/report/index","meta":{"navigationBar":{"titleText":"报表","type":"default"},"isNVue":false}}].map(uniRoute=>(uniRoute.meta.route=uniRoute.path,__uniConfig.pages.push(uniRoute.path),uniRoute.path='/'+uniRoute.path,uniRoute)); const __uniRoutes = [{"path":"pages/index/index","meta":{"isQuit":true,"isEntry":true,"isTabBar":true,"tabBarIndex":0,"navigationBar":{"titleText":"五金配件管家","type":"default"},"isNVue":false}},{"path":"pages/order/create","meta":{"isQuit":true,"isTabBar":true,"tabBarIndex":2,"navigationBar":{"titleText":"开单","type":"default"},"isNVue":false}},{"path":"pages/product/select","meta":{"navigationBar":{"titleText":"选择商品","type":"default"},"isNVue":false}},{"path":"pages/product/list","meta":{"isQuit":true,"isTabBar":true,"tabBarIndex":1,"navigationBar":{"titleText":"货品列表","type":"default"},"isNVue":false}},{"path":"pages/product/submit","meta":{"navigationBar":{"backgroundColor":"#ffffff","titleText":"提交配件","type":"default"},"isNVue":false}},{"path":"pages/product/submissions","meta":{"navigationBar":{"titleText":"我的提交","type":"default"},"isNVue":false}},{"path":"pages/product/submission-detail","meta":{"navigationBar":{"titleText":"提交详情","type":"default"},"isNVue":false}},{"path":"pages/product/form","meta":{"navigationBar":{"titleText":"编辑货品","type":"default"},"isNVue":false}},{"path":"pages/product/product-detail","meta":{"navigationBar":{"titleText":"货品详情","type":"default"},"isNVue":false}},{"path":"pages/product/categories","meta":{"navigationBar":{"titleText":"类别管理","type":"default"},"isNVue":false}},{"path":"pages/product/units","meta":{"navigationBar":{"titleText":"单位管理","type":"default"},"isNVue":false}},{"path":"pages/product/settings","meta":{"navigationBar":{"titleText":"货品设置","type":"default"},"isNVue":false}},{"path":"pages/customer/select","meta":{"navigationBar":{"titleText":"选择客户","type":"default"},"isNVue":false}},{"path":"pages/customer/form","meta":{"navigationBar":{"titleText":"新增客户","type":"default"},"isNVue":false}},{"path":"pages/customer/detail","meta":{"navigationBar":{"titleText":"客户详情","type":"default"},"isNVue":false}},{"path":"pages/supplier/select","meta":{"navigationBar":{"titleText":"选择供应商","type":"default"},"isNVue":false}},{"path":"pages/supplier/form","meta":{"navigationBar":{"titleText":"新增/编辑供应商","type":"default"},"isNVue":false}},{"path":"pages/account/select","meta":{"navigationBar":{"titleText":"选择账户","type":"default"},"isNVue":false}},{"path":"pages/account/ledger","meta":{"navigationBar":{"titleText":"账户流水","type":"default"},"isNVue":false}},{"path":"pages/account/form","meta":{"navigationBar":{"titleText":"新增/编辑账户","type":"default"},"isNVue":false}},{"path":"pages/detail/index","meta":{"isQuit":true,"isTabBar":true,"tabBarIndex":3,"navigationBar":{"titleText":"明细","type":"default"},"isNVue":false}},{"path":"pages/auth/login","meta":{"navigationBar":{"titleText":"登录","type":"default"},"isNVue":false}},{"path":"pages/auth/register","meta":{"navigationBar":{"titleText":"注册","type":"default"},"isNVue":false}},{"path":"pages/my/index","meta":{"isQuit":true,"isTabBar":true,"tabBarIndex":4,"navigationBar":{"titleText":"我的","type":"default"},"isNVue":false}},{"path":"pages/my/about","meta":{"navigationBar":{"titleText":"关于与协议","type":"default"},"isNVue":false}},{"path":"pages/my/security","meta":{"navigationBar":{"titleText":"账号与安全","type":"default"},"isNVue":false}},{"path":"pages/my/vip","meta":{"backgroundColor":"#1a1a2e","backgroundTextStyle":"light","navigationBar":{"backgroundColor":"#1a1a2e","titleText":"VIP会员","type":"default","titleColor":"#ffffff"},"isNVue":false}},{"path":"pages/my/orders","meta":{"navigationBar":{"titleText":"我的订单","type":"default"},"isNVue":false}},{"path":"pages/report/index","meta":{"navigationBar":{"titleText":"报表","type":"default"},"isNVue":false}}].map(uniRoute=>(uniRoute.meta.route=uniRoute.path,__uniConfig.pages.push(uniRoute.path),uniRoute.path='/'+uniRoute.path,uniRoute));
__uniConfig.styles=[];//styles __uniConfig.styles=[];//styles
__uniConfig.onReady=function(callback){if(__uniConfig.ready){callback()}else{onReadyCallbacks.push(callback)}};Object.defineProperty(__uniConfig,"ready",{get:function(){return isReady},set:function(val){isReady=val;if(!isReady){return}const callbacks=onReadyCallbacks.slice(0);onReadyCallbacks.length=0;callbacks.forEach(function(callback){callback()})}}); __uniConfig.onReady=function(callback){if(__uniConfig.ready){callback()}else{onReadyCallbacks.push(callback)}};Object.defineProperty(__uniConfig,"ready",{get:function(){return isReady},set:function(val){isReady=val;if(!isReady){return}const callbacks=onReadyCallbacks.slice(0);onReadyCallbacks.length=0;callbacks.forEach(function(callback){callback()})}});
__uniConfig.onServiceReady=function(callback){if(__uniConfig.serviceReady){callback()}else{onServiceReadyCallbacks.push(callback)}};Object.defineProperty(__uniConfig,"serviceReady",{get:function(){return isServiceReady},set:function(val){isServiceReady=val;if(!isServiceReady){return}const callbacks=onServiceReadyCallbacks.slice(0);onServiceReadyCallbacks.length=0;callbacks.forEach(function(callback){callback()})}}); __uniConfig.onServiceReady=function(callback){if(__uniConfig.serviceReady){callback()}else{onServiceReadyCallbacks.push(callback)}};Object.defineProperty(__uniConfig,"serviceReady",{get:function(){return isServiceReady},set:function(val){isServiceReady=val;if(!isServiceReady){return}const callbacks=onServiceReadyCallbacks.slice(0);onServiceReadyCallbacks.length=0;callbacks.forEach(function(callback){callback()})}});

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -72,14 +72,14 @@
"statusbar": { "statusbar": {
"immersed": "supportedDevice", "immersed": "supportedDevice",
"style": "dark", "style": "dark",
"background": "#F8F8F8" "background": "#FFFFFF"
}, },
"uniStatistics": { "uniStatistics": {
"enable": false "enable": false
}, },
"allowsInlineMediaPlayback": true, "allowsInlineMediaPlayback": true,
"safearea": { "safearea": {
"background": "#ffffff", "background": "#FFFFFF",
"bottom": { "bottom": {
"offset": "auto" "offset": "auto"
} }
@@ -100,8 +100,8 @@
}, },
"tabBar": { "tabBar": {
"position": "bottom", "position": "bottom",
"color": "#8a7535", "color": "#444444",
"selectedColor": "#B4880F", "selectedColor": "#4C8DFF",
"borderStyle": "rgba(0,0,0,0.4)", "borderStyle": "rgba(0,0,0,0.4)",
"blurEffect": "none", "blurEffect": "none",
"fontSize": "10px", "fontSize": "10px",
@@ -112,35 +112,35 @@
{ {
"pagePath": "pages/index/index", "pagePath": "pages/index/index",
"text": "首页", "text": "首页",
"iconPath": "/static/logo.png", "iconPath": "/static/icons/home.png",
"selectedIconPath": "/static/logo.png" "selectedIconPath": "/static/icons/home.png"
}, },
{ {
"pagePath": "pages/product/list", "pagePath": "pages/product/list",
"text": "货品", "text": "货品",
"iconPath": "/static/logo.png", "iconPath": "/static/icons/product.png",
"selectedIconPath": "/static/logo.png" "selectedIconPath": "/static/icons/product.png"
}, },
{ {
"pagePath": "pages/order/create", "pagePath": "pages/order/create",
"text": "开单", "text": "开单",
"iconPath": "/static/logo.png", "iconPath": "/static/icons/icons8-purchase-order-100.png",
"selectedIconPath": "/static/logo.png" "selectedIconPath": "/static/icons/icons8-purchase-order-100.png"
}, },
{ {
"pagePath": "pages/detail/index", "pagePath": "pages/detail/index",
"text": "明细", "text": "明细",
"iconPath": "/static/logo.png", "iconPath": "/static/icons/icons8-more-details-100.png",
"selectedIconPath": "/static/logo.png" "selectedIconPath": "/static/icons/icons8-more-details-100.png"
}, },
{ {
"pagePath": "pages/my/index", "pagePath": "pages/my/index",
"text": "我的", "text": "我的",
"iconPath": "/static/logo.png", "iconPath": "/static/icons/icons8-account-male-100.png",
"selectedIconPath": "/static/logo.png" "selectedIconPath": "/static/icons/icons8-account-male-100.png"
} }
], ],
"backgroundColor": "#ffffff", "backgroundColor": "#FFFFFF",
"selectedIndex": 0, "selectedIndex": 0,
"shown": true, "shown": true,
"child": [ "child": [
@@ -158,7 +158,7 @@
"enable": false "enable": false
}, },
"safearea": { "safearea": {
"background": "#ffffff", "background": "#FFFFFF",
"bottom": { "bottom": {
"offset": "auto" "offset": "auto"
} }

View File

@@ -1,23 +1,77 @@
/**
.page { display:flex; flex-direction: column; height: 100vh; * 这里是uni-app内置的常用样式变量
*
* uni-app 官方扩展插件及插件市场https://ext.dcloud.net.cn上很多三方插件均使用了这些样式变量
* 如果你是插件开发者建议你使用scss预处理并在插件代码中直接使用这些变量无需 import 这个文件方便用户通过搭积木的方式开发整体风格一致的App
*
*/
/**
* 如果你是App开发者插件使用者你可以通过修改这些变量来定制自己的插件主题实现自定义主题功能
*
* 如果你的项目同样使用了scss预处理你也可以直接在你的 scss 代码中使用如下变量,同时无需 import 这个文件
*/
/* 颜色变量 */
/* 行为相关颜色 */
/* 藏青系主色(高亮) */
/* 文字基本颜色 */
/* 背景颜色 */
/* 边框颜色 */
/* 尺寸变量 */
/* 文字尺寸 */
/* 图片尺寸 */
/* Border Radius */
/* 水平间距 */
/* 垂直间距 */
/* 透明度 */
/* 文章场景相关 */
/* 表单控件尺寸(统一配置,避免页面内硬编码) */
.page {
display: flex;
flex-direction: column;
height: 100vh;
} }
.form { background:#fff; .form {
background: #ffffff;
} }
.field { display:flex; align-items:center; justify-content: space-between; padding: 0.5625rem 0.625rem; border-bottom:0.03125rem solid #f3f3f3; .field {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.5625rem 0.625rem;
border-bottom: 0.03125rem solid #e5e7eb;
} }
.label { color:#666; .label {
color: #444;
} }
.input { flex:1; text-align: right; color:#333; .input {
flex: 1;
text-align: right;
color: #111;
} }
.value { color:#333; .value {
color: #111;
} }
.actions { margin-top: 0.625rem; padding: 0 0.625rem; .actions {
margin-top: 0.625rem;
padding: 0 0.625rem;
} }
.primary { width: 100%; background: #3c9cff; color:#fff; border-radius: 0.25rem; padding: 0.6875rem 0; .primary {
width: 100%;
background: #4C8DFF;
color: #fff;
border-radius: 0.25rem;
padding: 0.6875rem 0;
} }
.sheet { background:#fff; .sheet {
background: #ffffff;
} }
.sheet-item { padding: 0.8125rem; text-align:center; border-bottom:0.03125rem solid #f2f2f2; .sheet-item {
} padding: 0.8125rem;
.sheet-cancel { padding: 0.8125rem; text-align:center; color:#666; text-align: center;
border-bottom: 0.03125rem solid #e5e7eb;
} }
.sheet-cancel {
padding: 0.8125rem;
text-align: center;
color: #444;
}

View File

@@ -1,35 +1,107 @@
/**
.page { display:flex; flex-direction: column; height: 100vh; * 这里是uni-app内置的常用样式变量
*
* uni-app 官方扩展插件及插件市场https://ext.dcloud.net.cn上很多三方插件均使用了这些样式变量
* 如果你是插件开发者建议你使用scss预处理并在插件代码中直接使用这些变量无需 import 这个文件方便用户通过搭积木的方式开发整体风格一致的App
*
*/
/**
* 如果你是App开发者插件使用者你可以通过修改这些变量来定制自己的插件主题实现自定义主题功能
*
* 如果你的项目同样使用了scss预处理你也可以直接在你的 scss 代码中使用如下变量,同时无需 import 这个文件
*/
/* 颜色变量 */
/* 行为相关颜色 */
/* 藏青系主色(高亮) */
/* 文字基本颜色 */
/* 背景颜色 */
/* 边框颜色 */
/* 尺寸变量 */
/* 文字尺寸 */
/* 图片尺寸 */
/* Border Radius */
/* 水平间距 */
/* 垂直间距 */
/* 透明度 */
/* 文章场景相关 */
/* 表单控件尺寸(统一配置,避免页面内硬编码) */
.page {
display: flex;
flex-direction: column;
height: 100vh;
} }
.filters { display:flex; gap: 0.5rem; padding: 0.5rem; background:#fff; .filters {
display: flex;
gap: 0.5rem;
padding: 0.5rem;
background: #ffffff;
} }
.field { display:flex; justify-content: space-between; align-items:center; padding: 0.5rem; border:0.03125rem solid #eee; border-radius: 0.375rem; min-width: 9.375rem; .field {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.5rem;
border: 0.03125rem solid #e5e7eb;
border-radius: 0.375rem;
min-width: 9.375rem;
} }
.label { color:#666; .label {
color: #444;
} }
.value { color:#333; .value {
color: #111;
} }
.summary { display:grid; grid-template-columns: repeat(4,1fr); gap: 0.375rem; padding: 0.375rem 0.5rem; background:#fff; border-top:0.03125rem solid #f1f1f1; border-bottom:0.03125rem solid #f1f1f1; .summary {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 0.375rem;
padding: 0.375rem 0.5rem;
background: #ffffff;
border-top: 0.03125rem solid #e5e7eb;
border-bottom: 0.03125rem solid #e5e7eb;
} }
.sum-item { padding: 0.375rem; text-align:center; .sum-item {
padding: 0.375rem;
text-align: center;
} }
.k { display:block; color:#888; font-size: 0.75rem; .k {
display: block;
color: #444;
font-size: 0.75rem;
} }
.v { display:block; margin-top:0.1875rem; font-weight:700; color:#333; .v {
display: block;
margin-top: 0.1875rem;
font-weight: 700;
color: #111;
} }
.list { flex:1; .list {
flex: 1;
} }
.item { padding: 0.5625rem 0.5rem; border-bottom:0.03125rem solid #f4f4f4; background:#fff; .item {
padding: 0.5625rem 0.5rem;
border-bottom: 0.03125rem solid #e5e7eb;
background: #ffffff;
} }
.row { display:flex; align-items:center; justify-content: space-between; margin-bottom: 0.1875rem; .row {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 0.1875rem;
} }
.title { color:#333; .title {
color: #111;
} }
.amount { font-weight:700; .amount {
font-weight: 700;
} }
.amount.in { color:#2a9d8f; .amount.in {
color: #2a9d8f;
} }
.amount.out { color:#d35b5b; .amount.out {
} color: #d35b5b;
.meta { color:#999; font-size: 0.75rem;
} }
.meta {
color: #444;
font-size: 0.75rem;
}

View File

@@ -1,13 +1,63 @@
/**
.page { display:flex; flex-direction: column; height: 100vh; * 这里是uni-app内置的常用样式变量
*
* uni-app 官方扩展插件及插件市场https://ext.dcloud.net.cn上很多三方插件均使用了这些样式变量
* 如果你是插件开发者建议你使用scss预处理并在插件代码中直接使用这些变量无需 import 这个文件方便用户通过搭积木的方式开发整体风格一致的App
*
*/
/**
* 如果你是App开发者插件使用者你可以通过修改这些变量来定制自己的插件主题实现自定义主题功能
*
* 如果你的项目同样使用了scss预处理你也可以直接在你的 scss 代码中使用如下变量,同时无需 import 这个文件
*/
/* 颜色变量 */
/* 行为相关颜色 */
/* 藏青系主色(高亮) */
/* 文字基本颜色 */
/* 背景颜色 */
/* 边框颜色 */
/* 尺寸变量 */
/* 文字尺寸 */
/* 图片尺寸 */
/* Border Radius */
/* 水平间距 */
/* 垂直间距 */
/* 透明度 */
/* 文章场景相关 */
/* 表单控件尺寸(统一配置,避免页面内硬编码) */
.page {
display: flex;
flex-direction: column;
height: 100vh;
} }
.list { flex:1; .list {
flex: 1;
} }
.item { padding: 0.625rem 0.75rem; background:#fff; border-bottom: 0.03125rem solid #f1f1f1; .item {
padding: 0.625rem 0.75rem;
background: #ffffff;
border-bottom: 0.03125rem solid #e5e7eb;
} }
.name { color:#333; margin-bottom: 0.1875rem; .name {
color: #111;
margin-bottom: 0.1875rem;
} }
.meta { color:#888; font-size: 0.75rem; .meta {
} color: #444;
.fab { position: fixed; right: 1rem; bottom: 3.75rem; width: 3.125rem; height: 3.125rem; border-radius: 50%; background:#3c9cff; color:#fff; display:flex; align-items:center; justify-content:center; font-size: 1.625rem; box-shadow: 0 0.3125rem 0.625rem rgba(0,0,0,0.18); font-size: 0.75rem;
} }
.fab {
position: fixed;
right: 1rem;
bottom: 3.75rem;
width: 3.125rem;
height: 3.125rem;
border-radius: 50%;
background: #4C8DFF;
color: #fff;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.625rem;
box-shadow: 0 0.3125rem 0.625rem rgba(0, 0, 0, 0.18);
}

View File

@@ -0,0 +1,163 @@
/**
* 这里是uni-app内置的常用样式变量
*
* uni-app 官方扩展插件及插件市场https://ext.dcloud.net.cn上很多三方插件均使用了这些样式变量
* 如果你是插件开发者建议你使用scss预处理并在插件代码中直接使用这些变量无需 import 这个文件方便用户通过搭积木的方式开发整体风格一致的App
*
*/
/**
* 如果你是App开发者插件使用者你可以通过修改这些变量来定制自己的插件主题实现自定义主题功能
*
* 如果你的项目同样使用了scss预处理你也可以直接在你的 scss 代码中使用如下变量,同时无需 import 这个文件
*/
/* 颜色变量 */
/* 行为相关颜色 */
/* 藏青系主色(高亮) */
/* 文字基本颜色 */
/* 背景颜色 */
/* 边框颜色 */
/* 尺寸变量 */
/* 文字尺寸 */
/* 图片尺寸 */
/* Border Radius */
/* 水平间距 */
/* 垂直间距 */
/* 透明度 */
/* 文章场景相关 */
/* 表单控件尺寸(统一配置,避免页面内硬编码) */
/**
* 这里是uni-app内置的常用样式变量
*
* uni-app 官方扩展插件及插件市场https://ext.dcloud.net.cn上很多三方插件均使用了这些样式变量
* 如果你是插件开发者建议你使用scss预处理并在插件代码中直接使用这些变量无需 import 这个文件方便用户通过搭积木的方式开发整体风格一致的App
*
*/
/**
* 如果你是App开发者插件使用者你可以通过修改这些变量来定制自己的插件主题实现自定义主题功能
*
* 如果你的项目同样使用了scss预处理你也可以直接在你的 scss 代码中使用如下变量,同时无需 import 这个文件
*/
/* 颜色变量 */
/* 行为相关颜色 */
/* 藏青系主色(高亮) */
/* 文字基本颜色 */
/* 背景颜色 */
/* 边框颜色 */
/* 尺寸变量 */
/* 文字尺寸 */
/* 图片尺寸 */
/* Border Radius */
/* 水平间距 */
/* 垂直间距 */
/* 透明度 */
/* 文章场景相关 */
/* 表单控件尺寸(统一配置,避免页面内硬编码) */
.auth-page {
padding: 1rem;
display: flex;
flex-direction: column;
gap: 0.75rem;
position: relative;
min-height: 100vh;
}
.header {
display: flex;
align-items: center;
justify-content: center;
padding: 0.25rem 0 0;
}
.title {
font-size: 1.0625rem;
font-weight: 800;
color: #111;
}
.login-hero {
display: flex;
justify-content: center;
padding: 0.5rem 0 0;
}
.login-hero-img {
width: 72%;
max-width: 17.5rem;
border-radius: 0.25rem;
}
.panel {
display: flex;
flex-direction: column;
gap: 0.5rem;
background: transparent;
padding: 0;
border-radius: 0;
border: none;
}
.panel.minor {
margin-top: 0.375rem;
}
.input {
background: #ffffff;
border: 0.0625rem solid #e5e7eb;
border-radius: 0.375rem;
padding: 0.6875rem 0.625rem;
font-size: 0.875rem;
}
.row {
display: flex;
gap: 0.375rem;
align-items: center;
}
.flex1 {
flex: 1;
}
.btn {
padding: 0.6875rem 0.625rem;
border-radius: 0.375rem;
font-weight: 800;
text-align: center;
}
.btn.primary {
background: linear-gradient(135deg, #4788ff 0%, #2d6be6 100%);
color: #fff;
border: 0.03125rem solid rgba(45, 107, 230, 0.25);
width: 72%;
margin: 0 auto;
padding: 0.4375rem 0.5rem;
}
.btn.ghost {
background: #eef3ff;
color: #2d6be6;
}
/* 右下角快捷入口:贴着登录功能,无边框、无背景 */
.quick-inline {
display: flex;
gap: 0.875rem;
justify-content: flex-end;
align-items: center;
margin-top: 0.3125rem;
}
.quick-link {
background: transparent !important;
color: #2d6be6;
border: none !important;
outline: none;
padding: 0;
font-size: 0.8125rem;
font-weight: 700;
box-shadow: none;
line-height: 1.2;
}
.quick-link::after {
border: none !important;
}
/* 注册/重置页:验证码按钮与左侧输入框等高,且更紧凑 */
.panel.minor .row > .input {
height: 2.375rem;
padding: 0 0.625rem;
}
.panel.minor .row > .btn.ghost {
height: 2.375rem;
padding: 0 0.625rem;
border-radius: 0.375rem;
display: inline-flex;
align-items: center;
justify-content: center;
}

View File

@@ -0,0 +1,292 @@
/**
* 这里是uni-app内置的常用样式变量
*
* uni-app 官方扩展插件及插件市场https://ext.dcloud.net.cn上很多三方插件均使用了这些样式变量
* 如果你是插件开发者建议你使用scss预处理并在插件代码中直接使用这些变量无需 import 这个文件方便用户通过搭积木的方式开发整体风格一致的App
*
*/
/**
* 如果你是App开发者插件使用者你可以通过修改这些变量来定制自己的插件主题实现自定义主题功能
*
* 如果你的项目同样使用了scss预处理你也可以直接在你的 scss 代码中使用如下变量,同时无需 import 这个文件
*/
/* 颜色变量 */
/* 行为相关颜色 */
/* 藏青系主色(高亮) */
/* 文字基本颜色 */
/* 背景颜色 */
/* 边框颜色 */
/* 尺寸变量 */
/* 文字尺寸 */
/* 图片尺寸 */
/* Border Radius */
/* 水平间距 */
/* 垂直间距 */
/* 透明度 */
/* 文章场景相关 */
/* 表单控件尺寸(统一配置,避免页面内硬编码) */
.register-container {
position: relative;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 1.25rem 0.625rem;
overflow: hidden;
}
.background-decoration {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 0;
}
.background-decoration .circle {
position: absolute;
border-radius: 50%;
background: rgba(255, 255, 255, 0.1);
}
.background-decoration .circle.circle-1 {
width: 6.875rem;
height: 6.875rem;
top: 8%;
left: 12%;
animation: float 7s ease-in-out infinite;
}
.background-decoration .circle.circle-2 {
width: 5.625rem;
height: 5.625rem;
top: 65%;
right: 10%;
animation: float 9s ease-in-out infinite reverse;
}
.background-decoration .circle.circle-3 {
width: 3.75rem;
height: 3.75rem;
bottom: 15%;
left: 25%;
animation: float 6s ease-in-out infinite;
}
@keyframes float {
0%, 100% {
transform: translateY(0px) rotate(0deg);
}
50% {
transform: translateY(-25px) rotate(5deg);
}
}
.register-card {
position: relative;
z-index: 1;
width: 90%;
max-width: 21.25rem;
background: rgba(255, 255, 255, 0.95);
-webkit-backdrop-filter: blur(0.625rem);
backdrop-filter: blur(0.625rem);
border-radius: 1rem;
padding: 1.5625rem 1.25rem 1.40625rem;
box-shadow: 0 0.78125rem 2.1875rem rgba(0, 0, 0, 0.12);
border: 0.03125rem solid rgba(255, 255, 255, 0.3);
}
.header-section {
text-align: center;
margin-bottom: 1.40625rem;
}
.header-section .logo-container {
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
margin-bottom: 0.625rem;
}
.header-section .logo-container .logo-icon {
width: 1.875rem;
height: 1.875rem;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 0.5rem;
display: flex;
align-items: center;
justify-content: center;
}
.header-section .logo-container .logo-icon .icon {
width: 1.125rem;
height: 1.125rem;
fill: white;
}
.header-section .logo-container .app-name {
font-size: 1.125rem;
font-weight: 700;
color: #2d3748;
letter-spacing: 0.03125rem;
}
.header-section .welcome-text {
display: block;
font-size: 1.5rem;
font-weight: 700;
color: #2d3748;
margin-bottom: 0.25rem;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.header-section .subtitle {
display: block;
font-size: 0.875rem;
color: #718096;
font-weight: 400;
}
.form-section {
margin-bottom: 1.25rem;
}
.form-section .input-group {
margin-bottom: 0.75rem;
}
.form-section .input-group .input-container {
position: relative;
background: #f7fafc;
border: 0.0625rem solid #e2e8f0;
border-radius: 0.5rem;
display: flex;
align-items: center;
transition: all 0.3s ease;
}
.form-section .input-group .input-container.focused {
border-color: #667eea;
background: #ffffff;
box-shadow: 0 0 0 0.1875rem rgba(102, 126, 234, 0.1);
transform: translateY(-0.0625rem);
}
.form-section .input-group .input-container.filled {
background: #ffffff;
border-color: #cbd5e0;
}
.form-section .input-group .input-container .input-icon {
display: flex;
align-items: center;
justify-content: center;
width: 1.5625rem;
margin-left: 0.625rem;
}
.form-section .input-group .input-container .input-icon .icon {
width: 1rem;
height: 1rem;
fill: #a0aec0;
transition: fill 0.3s ease;
}
.form-section .input-group .input-container.focused .input-icon .icon {
fill: #667eea;
}
.form-section .input-group .input-container .input-field {
flex: 1;
background: transparent;
border: none;
padding: 0.75rem 0.625rem 0.75rem 0.375rem;
font-size: 1rem;
color: #2d3748;
}
.form-section .input-group .input-container .input-field::-webkit-input-placeholder {
color: #a0aec0;
font-size: 0.875rem;
}
.form-section .input-group .input-container .input-field::placeholder {
color: #a0aec0;
font-size: 0.875rem;
}
.actions-section {
margin-bottom: 0.9375rem;
}
.actions-section .register-button {
width: 100%;
height: 3rem;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border: none;
border-radius: 0.5rem;
margin-bottom: 0.625rem;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 0.25rem 0.75rem rgba(102, 126, 234, 0.3);
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}
.actions-section .register-button:active {
transform: translateY(0.0625rem);
box-shadow: 0 0.125rem 0.5rem rgba(102, 126, 234, 0.3);
}
.actions-section .register-button::before {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(135deg, rgba(255, 255, 255, 0.2) 0%, transparent 50%);
opacity: 0;
transition: opacity 0.3s ease;
}
.actions-section .register-button:active::before {
opacity: 1;
}
.actions-section .register-button .button-text {
font-size: 1rem;
font-weight: 600;
color: white;
letter-spacing: 0.03125rem;
}
.actions-section .login-button {
width: 100%;
height: 2.6875rem;
background: transparent;
border: 0.0625rem solid #e2e8f0;
border-radius: 0.5rem;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
}
.actions-section .login-button:active {
background: #f7fafc;
border-color: #cbd5e0;
transform: translateY(0.03125rem);
}
.actions-section .login-button .button-text {
font-size: 0.875rem;
font-weight: 500;
color: #718096;
}
.footer-section {
text-align: center;
}
.footer-section .hint-text {
display: block;
font-size: 0.75rem;
color: #a0aec0;
line-height: 1.6;
margin-bottom: 0.375rem;
}
.footer-section .static-hint {
display: block;
font-size: 0.6875rem;
color: #a0aec0;
line-height: 1.5;
background: rgba(160, 174, 192, 0.1);
padding: 0.375rem 0.5rem;
border-radius: 0.3125rem;
border: 0.03125rem solid rgba(160, 174, 192, 0.2);
}
@media (max-width: 750rpx) {
.register-card {
margin: 0.625rem;
padding: 1.25rem 0.9375rem 1.09375rem;
}
.header-section .welcome-text {
font-size: 1.3125rem;
}
.form-section .input-group {
margin-bottom: 0.625rem;
}
}

View File

@@ -1,23 +1,88 @@
/**
.page { padding-bottom: 4.375rem; * 这里是uni-app内置的常用样式变量
*
* uni-app 官方扩展插件及插件市场https://ext.dcloud.net.cn上很多三方插件均使用了这些样式变量
* 如果你是插件开发者建议你使用scss预处理并在插件代码中直接使用这些变量无需 import 这个文件方便用户通过搭积木的方式开发整体风格一致的App
*
*/
/**
* 如果你是App开发者插件使用者你可以通过修改这些变量来定制自己的插件主题实现自定义主题功能
*
* 如果你的项目同样使用了scss预处理你也可以直接在你的 scss 代码中使用如下变量,同时无需 import 这个文件
*/
/* 颜色变量 */
/* 行为相关颜色 */
/* 藏青系主色(高亮) */
/* 文字基本颜色 */
/* 背景颜色 */
/* 边框颜色 */
/* 尺寸变量 */
/* 文字尺寸 */
/* 图片尺寸 */
/* Border Radius */
/* 水平间距 */
/* 垂直间距 */
/* 透明度 */
/* 文章场景相关 */
/* 表单控件尺寸(统一配置,避免页面内硬编码) */
.page {
padding-bottom: 4.375rem;
} }
.card { background:#fff; margin: 0.5rem; padding: 0.375rem 0.5rem; border-radius: 0.5rem; .card {
background: #ffffff;
margin: 0.5rem;
padding: 0.375rem 0.5rem;
border-radius: 0.5rem;
} }
.row { display:flex; justify-content: space-between; padding: 0.5625rem 0.25rem; border-bottom: 0.03125rem solid #f3f3f3; .row {
display: flex;
justify-content: space-between;
padding: 0.5625rem 0.25rem;
border-bottom: 0.03125rem solid #e5e7eb;
} }
.row:last-child { border-bottom: 0; .row:last-child {
border-bottom: 0;
} }
.label { color:#666; .label {
color: #444;
} }
.value { color:#333; max-width: 60%; text-align: right; .value {
color: #111;
max-width: 60%;
text-align: right;
} }
.value-input { color:#333; text-align: right; flex: 1; .value-input {
color: #111;
text-align: right;
flex: 1;
} }
.emp { color:#107e9b; font-weight: 700; .emp {
color: #4C8DFF;
font-weight: 700;
} }
.bottom { position: fixed; left:0; right:0; bottom:0; background:#fff; padding: 0.5rem 0.75rem calc(env(safe-area-inset-bottom) + 0.5rem); box-shadow: 0 -0.125rem 0.375rem rgba(0,0,0,0.06); display:flex; gap: 0.375rem; .bottom {
position: fixed;
left: 0;
right: 0;
bottom: 0;
background: #ffffff;
padding: 0.5rem 0.75rem calc(env(safe-area-inset-bottom) + 0.5rem);
box-shadow: 0 -0.125rem 0.375rem rgba(0, 0, 0, 0.1);
display: flex;
gap: 0.375rem;
} }
.primary { flex:1; background: linear-gradient(135deg, #A0E4FF 0%, #17A2C4 100%); color:#fff; border-radius: 31.21875rem; padding: 0.625rem 0; .primary {
} flex: 1;
.ghost { flex:1; background:#fff; color:#107e9b; border: 0.0625rem solid #A0E4FF; border-radius: 31.21875rem; padding: 0.5625rem 0; background: #4C8DFF;
color: #fff;
border-radius: 31.21875rem;
padding: 0.625rem 0;
} }
.ghost {
flex: 1;
background: #ffffff;
color: #4C8DFF;
border: 0.0625rem solid rgba(76, 141, 255, 0.35);
border-radius: 31.21875rem;
padding: 0.5625rem 0;
}

View File

@@ -1,15 +1,66 @@
/**
.page { padding-bottom: 4.375rem; * 这里是uni-app内置的常用样式变量
*
* uni-app 官方扩展插件及插件市场https://ext.dcloud.net.cn上很多三方插件均使用了这些样式变量
* 如果你是插件开发者建议你使用scss预处理并在插件代码中直接使用这些变量无需 import 这个文件方便用户通过搭积木的方式开发整体风格一致的App
*
*/
/**
* 如果你是App开发者插件使用者你可以通过修改这些变量来定制自己的插件主题实现自定义主题功能
*
* 如果你的项目同样使用了scss预处理你也可以直接在你的 scss 代码中使用如下变量,同时无需 import 这个文件
*/
/* 颜色变量 */
/* 行为相关颜色 */
/* 藏青系主色(高亮) */
/* 文字基本颜色 */
/* 背景颜色 */
/* 边框颜色 */
/* 尺寸变量 */
/* 文字尺寸 */
/* 图片尺寸 */
/* Border Radius */
/* 水平间距 */
/* 垂直间距 */
/* 透明度 */
/* 文章场景相关 */
/* 表单控件尺寸(统一配置,避免页面内硬编码) */
.page {
padding-bottom: 4.375rem;
} }
.field { display:flex; justify-content: space-between; padding: 0.6875rem 0.75rem; background:#fff; border-bottom:0.03125rem solid #eee; .field {
display: flex;
justify-content: space-between;
padding: 0.6875rem 0.75rem;
background: #ffffff;
border-bottom: 0.03125rem solid #e5e7eb;
} }
.label { color:#666; .label {
color: #444;
} }
.value { color:#333; text-align: right; flex: 1; .value {
color: #111;
text-align: right;
flex: 1;
} }
.textarea { padding: 0.5rem 0.75rem; background:#fff; margin-top: 0.375rem; .textarea {
padding: 0.5rem 0.75rem;
background: #ffffff;
margin-top: 0.375rem;
} }
.bottom { position: fixed; left:0; right:0; bottom:0; background:#fff; padding: 0.5rem 0.75rem calc(env(safe-area-inset-bottom) + 0.5rem); box-shadow: 0 -0.125rem 0.375rem rgba(0,0,0,0.06); .bottom {
} position: fixed;
.primary { width: 100%; background: linear-gradient(135deg, #A0E4FF 0%, #17A2C4 100%); color:#fff; border-radius: 31.21875rem; padding: 0.625rem 0; left: 0;
right: 0;
bottom: 0;
background: #ffffff;
padding: 0.5rem 0.75rem calc(env(safe-area-inset-bottom) + 0.5rem);
box-shadow: 0 -0.125rem 0.375rem rgba(0, 0, 0, 0.1);
} }
.primary {
width: 100%;
background: #4C8DFF;
color: #fff;
border-radius: 31.21875rem;
padding: 0.625rem 0;
}

View File

@@ -1,19 +1,78 @@
/**
.page { display:flex; flex-direction: column; height: 100vh; * 这里是uni-app内置的常用样式变量
*
* uni-app 官方扩展插件及插件市场https://ext.dcloud.net.cn上很多三方插件均使用了这些样式变量
* 如果你是插件开发者建议你使用scss预处理并在插件代码中直接使用这些变量无需 import 这个文件方便用户通过搭积木的方式开发整体风格一致的App
*
*/
/**
* 如果你是App开发者插件使用者你可以通过修改这些变量来定制自己的插件主题实现自定义主题功能
*
* 如果你的项目同样使用了scss预处理你也可以直接在你的 scss 代码中使用如下变量,同时无需 import 这个文件
*/
/* 颜色变量 */
/* 行为相关颜色 */
/* 藏青系主色(高亮) */
/* 文字基本颜色 */
/* 背景颜色 */
/* 边框颜色 */
/* 尺寸变量 */
/* 文字尺寸 */
/* 图片尺寸 */
/* Border Radius */
/* 水平间距 */
/* 垂直间距 */
/* 透明度 */
/* 文章场景相关 */
/* 表单控件尺寸(统一配置,避免页面内硬编码) */
.page {
display: flex;
flex-direction: column;
height: 100vh;
} }
.search { display:flex; gap: 0.375rem; padding: 0.5rem; background:#fff; align-items:center; .search {
display: flex;
gap: 0.375rem;
padding: 0.5rem;
background: #ffffff;
align-items: center;
} }
.search uni-input { flex:1; background:#f6f6f6; border-radius: 0.375rem; padding: 0.375rem; .search uni-input {
flex: 1;
background: #f1f1f1;
border-radius: 0.375rem;
padding: 0.375rem;
color: #111;
} }
.list { flex:1; .list {
flex: 1;
} }
.item { padding: 0.625rem 0.75rem; background:#fff; border-bottom: 0.03125rem solid #f1f1f1; .item {
padding: 0.625rem 0.75rem;
background: #ffffff;
border-bottom: 0.03125rem solid #e5e7eb;
} }
.name { color:#333; margin-bottom: 0.1875rem; .name {
color: #111;
margin-bottom: 0.1875rem;
} }
.meta { color:#888; font-size: 0.75rem; .meta {
color: #444;
font-size: 0.75rem;
} }
.bottom { position: fixed; left:0; right:0; bottom:0; background:#fff; padding: 0.5rem 0.75rem calc(env(safe-area-inset-bottom) + 0.5rem); box-shadow: 0 -0.125rem 0.375rem rgba(0,0,0,0.06); .bottom {
} position: fixed;
.primary { width: 100%; background: linear-gradient(135deg, #A0E4FF 0%, #17A2C4 100%); color:#fff; border-radius: 31.21875rem; padding: 0.625rem 0; left: 0;
right: 0;
bottom: 0;
background: #ffffff;
padding: 0.5rem 0.75rem calc(env(safe-area-inset-bottom) + 0.5rem);
box-shadow: 0 -0.125rem 0.375rem rgba(0, 0, 0, 0.1);
} }
.primary {
width: 100%;
background: #4C8DFF;
color: #fff;
border-radius: 31.21875rem;
padding: 0.625rem 0;
}

View File

@@ -1,49 +1,220 @@
/**
* 这里是uni-app内置的常用样式变量
*
* uni-app 官方扩展插件及插件市场https://ext.dcloud.net.cn上很多三方插件均使用了这些样式变量
* 如果你是插件开发者建议你使用scss预处理并在插件代码中直接使用这些变量无需 import 这个文件方便用户通过搭积木的方式开发整体风格一致的App
*
*/
/**
* 如果你是App开发者插件使用者你可以通过修改这些变量来定制自己的插件主题实现自定义主题功能
*
* 如果你的项目同样使用了scss预处理你也可以直接在你的 scss 代码中使用如下变量,同时无需 import 这个文件
*/
/* 颜色变量 */
/* 行为相关颜色 */
/* 藏青系主色(高亮) */
/* 文字基本颜色 */
/* 背景颜色 */
/* 边框颜色 */
/* 尺寸变量 */
/* 文字尺寸 */
/* 图片尺寸 */
/* Border Radius */
/* 水平间距 */
/* 垂直间距 */
/* 透明度 */
/* 文章场景相关 */
/* 表单控件尺寸(统一配置,避免页面内硬编码) */
.page {
display: flex;
flex-direction: column;
height: 100vh;
}
.page { display:flex; flex-direction: column; height: 100vh; /* 顶部分段(如需保留,可以隐藏或后续扩展) */
.seg {
display: none;
} }
.seg { display:flex; background:#fff; .content {
display: flex;
flex: 1;
min-height: 0;
} }
.seg-item { flex:1; padding: 0.6875rem 0; text-align:center; color:#666; .biz-tabs {
width: 4.375rem;
background: #fff;
border-right: 0.0625rem solid #e5e7eb;
display: flex;
flex-direction: column;
} }
.seg-item.active { color:#18b566; font-weight: 600; .biz {
flex: 0 0 3.75rem;
display: flex;
align-items: center;
justify-content: center;
color: #4C8DFF;
} }
.content { display:flex; flex:1; min-height: 0; .biz.active {
background: rgba(76, 141, 255, 0.1);
color: #4C8DFF;
font-weight: 700;
} }
.biz-tabs { width: 3.75rem; background:#eef6ff; display:flex; flex-direction: column; .panel {
flex: 1;
display: flex;
flex-direction: column;
background: #fff;
margin: 0.5rem;
border-radius: 0.5rem;
padding: 0.375rem;
} }
.biz { flex:0 0 3.75rem; display:flex; align-items:center; justify-content:center; color:#4aa3d6; .toolbar {
display: flex;
flex-direction: column;
gap: 0.3125rem;
padding: 0.3125rem 0.1875rem 0.1875rem;
border-bottom: 0.0625rem solid #e5e7eb;
} }
.biz.active { background:#3ac1c9; color:#fff; border-radius: 0 0.5rem 0.5rem 0; .period-group {
display: flex;
align-items: center;
gap: 0.25rem;
background: #f6f8fb;
border-radius: 0.3125rem;
padding: 0.25rem 0.3125rem;
} }
.panel { flex:1; display:flex; flex-direction: column; background:#fff; margin: 0.5rem; border-radius: 0.5rem; padding: 0.375rem; .period-label {
color: #6b778c;
} }
.toolbar { display:flex; align-items: center; gap: 0.375rem; padding: 0.25rem 0.1875rem; .date-chip {
padding: 0.25rem 0.375rem;
background: #fff;
border: 0.0625rem solid #e6ebf2;
border-radius: 0.25rem;
} }
.search { flex:1; .sep {
color: #99a2b3;
padding: 0 0.1875rem;
} }
.search-input { width:100%; background:#f6f6f6; border-radius: 0.375rem; padding: 0.375rem; .search-row {
display: flex;
align-items: center;
gap: 0.5rem;
} }
.period { color:#999; font-size: 0.75rem; padding: 0 0.1875rem; .search {
flex: 1;
min-width: 0;
display: flex;
} }
.total { color:#18b566; font-weight: 700; padding: 0.1875rem 0.1875rem 0.375rem; .search-input {
flex: 1;
height: 2.25rem;
line-height: 2.25rem;
padding: 0 0.75rem;
box-sizing: border-box;
background: #fff;
border-radius: 0.375rem;
color: #111;
border: 0.0625rem solid #e6ebf2;
font-size: 0.8125rem;
} }
.list { flex:1; .btn {
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
height: 2.25rem;
padding: 0 1rem;
margin-left: 0.125rem;
border-radius: 0.375rem;
background: #4C8DFF;
color: #fff;
border: none;
font-size: 0.8125rem;
box-sizing: border-box;
} }
.item { display:flex; align-items:center; padding: 0.625rem 0.3125rem; border-bottom: 0.03125rem solid #f1f1f1; .btn::after {
border: none;
} }
.item-left { flex:1; .total {
color: #4C8DFF;
font-weight: 700;
padding: 0.3125rem 0.1875rem 0.375rem;
background: #fff;
} }
.date { color:#999; font-size: 0.75rem; .list {
flex: 1;
} }
.name { color:#333; margin: 0.125rem 0; font-weight: 600; .loading {
text-align: center;
padding: 0.625rem 0;
color: #444;
} }
.no { color:#bbb; font-size: 0.6875rem; .finished {
text-align: center;
padding: 0.625rem 0;
color: #444;
} }
.amount { color:#333; font-weight: 700; .item {
display: grid;
grid-template-columns: 1fr auto auto;
align-items: center;
gap: 0.25rem;
padding: 0.5625rem 0.375rem;
border-bottom: 0.03125rem solid #e5e7eb;
} }
.arrow { color:#ccc; font-size: 1.25rem; margin-left: 0.25rem; .item-left {
display: flex;
flex-direction: column;
} }
.empty { height: 50vh; display:flex; align-items:center; justify-content:center; color:#999; .date {
color: #444;
font-size: 0.75rem;
} }
.fab { position: fixed; right: 0.9375rem; bottom: 3.75rem; width: 3.125rem; height: 3.125rem; background:#18b566; color:#fff; border-radius: 1.5625rem; text-align:center; line-height: 3.125rem; font-size: 1.5rem; box-shadow: 0 0.25rem 0.625rem rgba(0,0,0,0.15); .name {
color: #111;
margin: 0.125rem 0;
font-weight: 600;
} }
.no {
color: #99a2b3;
font-size: 0.6875rem;
}
.amount {
color: #111;
font-weight: 700;
text-align: right;
}
.amount.in {
color: #16a34a;
}
.amount.out {
color: #dc2626;
}
.arrow {
color: #8c99b0;
font-size: 1.25rem;
margin-left: 0.25rem;
}
.empty {
height: 50vh;
display: flex;
align-items: center;
justify-content: center;
color: #444;
}
.fab {
position: fixed;
right: 0.9375rem;
bottom: 3.75rem;
width: 3.125rem;
height: 3.125rem;
background: #fff;
color: #4C8DFF;
border: 0.0625rem solid #4C8DFF;
border-radius: 1.5625rem;
text-align: center;
line-height: 3.125rem;
font-size: 1.5rem;
box-shadow: 0 0.25rem 0.625rem rgba(76, 141, 255, 0.18);
}

View File

@@ -1,185 +1,418 @@
/**
* 这里是uni-app内置的常用样式变量
*
* uni-app 官方扩展插件及插件市场https://ext.dcloud.net.cn上很多三方插件均使用了这些样式变量
* 如果你是插件开发者建议你使用scss预处理并在插件代码中直接使用这些变量无需 import 这个文件方便用户通过搭积木的方式开发整体风格一致的App
*
*/
/**
* 如果你是App开发者插件使用者你可以通过修改这些变量来定制自己的插件主题实现自定义主题功能
*
* 如果你的项目同样使用了scss预处理你也可以直接在你的 scss 代码中使用如下变量,同时无需 import 这个文件
*/
/* 颜色变量 */
/* 行为相关颜色 */
/* 藏青系主色(高亮) */
/* 文字基本颜色 */
/* 背景颜色 */
/* 边框颜色 */
/* 尺寸变量 */
/* 文字尺寸 */
/* 图片尺寸 */
/* Border Radius */
/* 水平间距 */
/* 垂直间距 */
/* 透明度 */
/* 文章场景相关 */
/* 表单控件尺寸(统一配置,避免页面内硬编码) */
body {
height: 100%;
overflow: hidden;
background: linear-gradient(180deg, #f8fbff 0%, #ffffff 60%);
}
.home { .home {
padding-bottom: 4.375rem; height: 100vh;
position: relative; display: flex;
/* 明亮奢华背景:金属拉丝纹理覆盖层 + 柔和浅色渐变 */ flex-direction: column;
background: padding-bottom: calc(env(safe-area-inset-bottom) + 1rem);
repeating-linear-gradient(0deg, rgba(180,180,180,0.12) 0, rgba(180,180,180,0.12) 0.0625rem, rgba(255,255,255,0.0) 0.0625rem, rgba(255,255,255,0.0) 0.3125rem), position: relative;
linear-gradient(180deg, rgba(255, 255, 255, 0.75) 0%, rgba(255, 255, 255, 0.55) 40%, rgba(255, 255, 255, 0.35) 100%); /* 渐变背景:顶部淡蓝过渡到白色 */
min-height: 100vh; background: linear-gradient(180deg, #f8fbff 0%, #ffffff 60%);
} overflow: hidden;
.home-bg { box-sizing: border-box;
position: fixed;
left: 0; top: 0; right: 0; bottom: 0;
width: 100%; height: 100%;
pointer-events: none;
z-index: -1;
} }
/* 公告栏 */ /* 首页横幅(移除) */
/* 公告栏 */
.notice { .notice {
margin: 0 0.75rem 0.75rem; margin: 0 0.75rem 0.75rem;
padding: 0.625rem 0.6875rem; padding: 0.625rem 0.6875rem;
border-radius: 0.625rem; border-radius: 0.625rem;
background: rgba(255,255,255,0.78); background: #ffffff;
-webkit-backdrop-filter: blur(0.375rem); border: 0.0625rem solid #e5e7eb;
backdrop-filter: blur(0.375rem); display: flex;
border: 0.0625rem solid rgba(203, 166, 61, 0.28); align-items: center;
display: flex; gap: 0.5rem;
align-items: center;
gap: 0.5rem;
} }
.notice-left { .notice-left {
flex: 0 0 auto; flex: 0 0 auto;
display: inline-flex; align-items: center; justify-content: center; display: inline-flex;
min-width: 3rem; height: 1.375rem; align-items: center;
padding: 0 0.5rem; justify-content: center;
border-radius: 31.21875rem; min-width: 3.125rem;
background: linear-gradient(135deg, #FFE69A, #F4CF62); height: 1.625rem;
color: #3f320f; padding: 0 0.5rem;
font-size: 0.75rem; border-radius: 31.21875rem;
font-weight: 800; background: #4C8DFF;
color: #fff;
font-size: 0.875rem;
font-weight: 800;
} }
.notice-swiper { height: 2.25rem; flex: 1; .notice-swiper {
height: 2.25rem;
flex: 1;
} }
.notice-item { display: flex; align-items: center; gap: 0.375rem; min-height: 2.25rem; .notice-item {
display: flex;
align-items: center;
gap: 0.375rem;
min-height: 2.25rem;
} }
.notice-text { color: #4b3e19; font-size: 0.875rem; line-height: 1.125rem; font-weight: 600; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; .notice-text {
color: #111;
font-size: 0.875rem;
line-height: 1.125rem;
font-weight: 600;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
} }
.notice-tag { color: #B4880F; font-size: 0.6875rem; padding: 0.125rem 0.3125rem; border-radius: 31.21875rem; background: rgba(215,167,46,0.18); .notice-tag {
color: #4C8DFF;
font-size: 0.6875rem;
padding: 0.125rem 0.3125rem;
border-radius: 31.21875rem;
background: rgba(76, 141, 255, 0.18);
} }
/* 分割标题 */
/* 分割标题 */ .section-title {
.section-title { display: flex; align-items: center; gap: 0.5rem; padding: 0.3125rem 0.875rem 0; display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.3125rem 0.875rem 0;
flex: 0 0 auto;
} }
.section-title::before { content: ''; display: block; width: 0.25rem; height: 0.875rem; border-radius: 0.25rem; background: linear-gradient(180deg, #FFE69A, #D7A72E); .section-title::before {
content: "";
display: block;
width: 0.25rem;
height: 0.875rem;
border-radius: 0.25rem;
background: #4C8DFF;
} }
.section-text { color: #6b5a2a; font-size: 0.875rem; font-weight: 700; letter-spacing: 0.03125rem; .section-text {
color: #111;
font-size: 0.9375rem;
font-weight: 700;
letter-spacing: 0.03125rem;
} }
/* 顶部英雄区:浅色玻璃卡片,带金色描边与柔和阴影 */ /* 顶部英雄区:浅色玻璃卡片,带金色描边与柔和阴影 */
.hero { .hero {
margin: 0.75rem; margin: 0.5rem 0.625rem;
padding: 1rem; padding: 0.5625rem 0.5625rem 0.375rem;
border-radius: 0.875rem; border-radius: 0.625rem;
background: rgba(255, 255, 255, 0.65); background: #ffffff;
-webkit-backdrop-filter: blur(0.4375rem); border: 0.0625rem solid #e5e7eb;
backdrop-filter: blur(0.4375rem); box-shadow: none;
border: 0.0625rem solid rgba(203, 166, 61, 0.35); color: #111;
box-shadow: 0 0.375rem 0.875rem rgba(0, 0, 0, 0.10), 0 0 0 0.0625rem rgba(255,255,255,0.60) inset; flex: 0 0 auto;
color: #473c22;
} }
.hero-top { .hero-top {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
margin-bottom: 0.625rem; margin-bottom: 0.625rem;
}
.hero-sub {
display: flex;
gap: 0.375rem;
margin-bottom: 0.6875rem;
flex-wrap: wrap;
}
.chip {
padding: 0.25rem 0.5rem;
border-radius: 31.21875rem;
background: rgba(76, 141, 255, 0.1);
color: #4C8DFF;
font-size: 0.75rem;
font-weight: 700;
border: 0.0625rem solid rgba(76, 141, 255, 0.25);
} }
.brand { .brand {
font-size: 1.3125rem; font-size: 1.125rem;
font-weight: 700; font-weight: 700;
letter-spacing: 0.0625rem; letter-spacing: 0.03125rem;
color: #B4880F; /* 金色标题 */ color: #4C8DFF;
} }
.cta { .cta {
padding: 0.3125rem 0.6875rem; padding: 0.25rem 0.5625rem;
border-radius: 31.21875rem; border-radius: 31.21875rem;
background: linear-gradient(135deg, rgba(255, 220, 128, 0.65), rgba(255, 240, 190, 0.65)); background: #4C8DFF;
border: 0.0625rem solid rgba(203, 166, 61, 0.45); border: 0.0625rem solid #4C8DFF;
box-shadow: 0 0.1875rem 0.4375rem rgba(203, 166, 61, 0.25); box-shadow: none;
} }
.cta-text { color: #5a4712; font-size: 0.8125rem; .cta-text {
} color: #fff;
.kpi { display: flex; font-size: 0.9375rem;
} font-weight: 700;
.kpi-item { flex: 1; letter-spacing: 0.03125rem;
}
.kpi-label { opacity: 0.9; font-size: 0.75rem; color: #6b5a2a;
}
.kpi-value { display: block; margin-top: 0.375rem; font-size: 1.4375rem; font-weight: 800; color: #B4880F;
} }
/* 功能容器:整体玻璃面板,增强融入感 */ /* 简易弹层样式 */
.grid-wrap { .dialog-mask {
margin: 0 0.625rem 1rem; position: fixed;
padding: 0.875rem 0.625rem 0.375rem; left: 0;
border-radius: 0.75rem; right: 0;
background: rgba(255,255,255,0.55); top: 0;
-webkit-backdrop-filter: blur(0.3125rem); bottom: 0;
backdrop-filter: blur(0.3125rem); background: rgba(0, 0, 0, 0.45);
border: 0.0625rem solid rgba(203,166,61,0.22); display: flex;
box-shadow: 0 0.25rem 0.5625rem rgba(0,0,0,0.06); align-items: center;
justify-content: center;
z-index: 10000;
}
.dialog {
width: 82vw;
background: #fff;
border-radius: 0.5rem;
padding: 0.625rem;
border: 0.0625rem solid #eef2f6;
}
.dialog-title {
font-size: 1rem;
font-weight: 800;
color: #111;
margin-bottom: 0.5rem;
}
.dialog-textarea {
width: 100%;
min-height: 5.625rem;
border: 0.0625rem solid #e8eef8;
border-radius: 0.375rem;
padding: 0.375rem;
box-sizing: border-box;
}
.dialog-actions {
display: flex;
justify-content: flex-end;
gap: 0.5625rem;
margin-top: 0.5rem;
}
.btn {
padding: 0.3125rem 0.6875rem;
border-radius: 31.21875rem;
background: #f3f6fb;
color: #334155;
border: 0.0625rem solid #e2e8f0;
font-weight: 700;
}
.btn.primary {
background: #4C8DFF;
color: #fff;
border-color: #4C8DFF;
} }
/* 功能九宫格 */ /* KPI 卡片化布局:横向铺满 */
.kpi {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 0.375rem;
}
.kpi-item {
text-align: center;
background: #ffffff;
border: 0.0625rem solid #e5e7eb;
border-radius: 0.5rem;
padding: 0.5rem 0.25rem;
}
/* KPI 卡片(更扁平,降低高度) */
.kpi-grid {
gap: 0.375rem;
}
.kpi-card {
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: center;
gap: 0.25rem;
text-align: left;
padding: 0.375rem 0.4375rem;
border-radius: 0.4375rem;
background: #fff;
border: 0.0625rem solid #eef2f6;
box-shadow: 0 0.0625rem 0.25rem rgba(0, 0, 0, 0.04);
min-height: 3.75rem;
}
.kpi-icon {
width: 1.375rem;
height: 1.375rem;
opacity: 0.9;
}
.kpi-content {
display: flex;
flex-direction: column;
}
.kpi-label {
color: #6b778c;
font-weight: 700;
font-size: 0.75rem;
line-height: 0.9375rem;
}
.kpi-value {
color: #4C8DFF;
font-size: 1.0625rem;
line-height: 1.1875rem;
margin-top: 0;
font-weight: 800;
}
/* 常用功能:胶囊+阴影卡片样式的图标栅格(旧风格保留以防回退) */
.grid { .grid {
display: grid; grid-row-gap: 1.125rem;
grid-template-columns: repeat(3, 1fr); grid-column-gap: 0.8125rem;
grid-row-gap: 2rem; padding: 1rem 0.75rem 0.875rem;
grid-column-gap: 1.125rem;
padding: 1.25rem 0.875rem 0.875rem;
} }
.grid-item { display: flex; flex-direction: column; align-items: center; text-align: center; .grid-item {
} position: relative;
.icon { display: flex; align-items: center; justify-content: center; color: #6b5a2a; position: relative;
} }
.icon-squircle { .icon-squircle {
width: 4.125rem; height: 4.125rem; width: 4.375rem;
border-radius: 0.875rem; height: 4.375rem;
background: linear-gradient(145deg, rgba(255,255,255,0.92), rgba(255,255,255,0.70)); border-radius: 0.875rem;
-webkit-backdrop-filter: blur(0.375rem); background: #fff;
backdrop-filter: blur(0.375rem); border: 0.0625rem solid #e5e7eb;
border: 0.0625rem solid rgba(203,166,61,0.22); box-shadow: 0 0.3125rem 0.625rem rgba(0, 0, 0, 0.06);
box-shadow: 0 0.3125rem 0.75rem rgba(0,0,0,0.10), 0 0 0 0.0625rem rgba(255,255,255,0.65) inset;
overflow: hidden;
} }
.icon-squircle::before { content: ''; position: absolute; left: -30%; top: -40%; width: 160%; height: 70%; background: linear-gradient( to bottom, rgba(255,255,255,0.9), rgba(255,255,255,0.0) ); transform: rotate(12deg); .grid-chip {
} margin-top: 0.375rem;
.icon-img { width: 3rem; height: 3rem; padding: 0.1875rem 0.4375rem;
} border-radius: 31.21875rem;
.icon-emoji { font-size: 1.875rem; line-height: 1; background: rgba(76, 141, 255, 0.12);
} color: #4C8DFF;
.icon-placeholder { width: 2.625rem; height: 2.625rem; border-radius: 0.5625rem; background: font-size: 0.8125rem;
linear-gradient(135deg, rgba(212,175,55,0.18), rgba(255,255,255,0.0)), font-weight: 700;
repeating-linear-gradient(90deg, rgba(180,150,60,0.35) 0, rgba(180,150,60,0.35) 0.25rem, transparent 0.25rem, transparent 0.5rem),
repeating-linear-gradient(0deg, rgba(180,150,60,0.20) 0, rgba(180,150,60,0.20) 0.25rem, transparent 0.25rem, transparent 0.5rem);
box-shadow: inset 0 0 0 0.0625rem rgba(203,166,61,0.28);
}
.icon-text { font-size: 1.4375rem; font-weight: 700;
}
.grid-title { display: none;
}
.grid-chip { margin-top: 0.4375rem; padding: 0.1875rem 0.4375rem; border-radius: 31.21875rem; background: rgba(215,167,46,0.16); color: #5a4a1f; font-size: 0.6875rem;
} }
/* 底部操作条:浅色半透明 + 金色主按钮 */ /* 功能容器:更轻的留白 */
.grid-wrap {
flex: 1 1 auto;
display: flex;
align-items: stretch;
justify-content: center;
margin: 0.5rem 0.625rem 0.875rem;
padding: 1rem 0.9375rem;
border-radius: 0.8125rem;
background: rgba(255, 255, 255, 0.96);
border: 0.0625rem solid #edf2f9;
box-shadow: 0 0.375rem 0.875rem rgba(32, 75, 143, 0.1);
box-sizing: border-box;
}
/* 功能卡片宫格:方形竖排,图标在上文字在下(与截图一致) */
.feature-grid {
flex: 1 1 auto;
width: 100%;
height: 100%;
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
grid-auto-rows: 1fr;
gap: 1rem 0.875rem;
align-content: space-evenly;
justify-items: center;
}
.feature-card {
width: 5.25rem;
height: 5.5rem;
background: #fff;
border: 0.0625rem solid #eef2f6;
border-radius: 0.625rem;
box-shadow: 0 0.3125rem 0.75rem rgba(0, 0, 0, 0.05);
padding: 0.5625rem 0.5rem;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.fc-icon {
width: 2.4375rem;
height: 2.4375rem;
border-radius: 0.5625rem;
background: #f7faff;
display: flex;
align-items: center;
justify-content: center;
}
.fc-img {
width: 1.6875rem;
height: 1.6875rem;
opacity: 0.95;
}
.fc-emoji {
font-size: 1.5rem;
}
.fc-placeholder {
width: 1.75rem;
height: 1.75rem;
border-radius: 0.375rem;
background: #f1f1f1;
border: 0.0625rem solid #e8eef8;
}
.fc-title {
margin-top: 0.375rem;
font-size: 0.875rem;
font-weight: 700;
color: #111;
letter-spacing: 0.03125rem;
}
/* 底部操作条:浅色半透明 + 金色主按钮 */
.bottom-bar { .bottom-bar {
position: fixed; position: fixed;
left: 0; right: 0; bottom: 0; left: 0;
display: flex; right: 0;
align-items: center; bottom: 0;
justify-content: space-around; display: flex;
padding: 0.4375rem 0.5625rem calc(env(safe-area-inset-bottom) + 0.4375rem); align-items: center;
background: rgba(255,255,255,0.85); justify-content: space-around;
box-shadow: 0 -0.1875rem 0.5625rem rgba(0,0,0,0.08); padding: 0.4375rem 0.5625rem calc(env(safe-area-inset-bottom) + 0.4375rem);
-webkit-backdrop-filter: blur(0.3125rem); background: rgba(255, 255, 255, 0.85);
backdrop-filter: blur(0.3125rem); box-shadow: 0 -0.1875rem 0.5625rem rgba(0, 0, 0, 0.08);
z-index: 9999; -webkit-backdrop-filter: blur(0.3125rem);
backdrop-filter: blur(0.3125rem);
z-index: 9999;
} }
.tab { flex: 1; text-align: center; color: #8a7535; font-size: 0.8125rem; .tab {
flex: 1;
text-align: center;
color: #8a7535;
font-size: 0.8125rem;
} }
.tab.active { color: #B4880F; .tab.active {
color: #B4880F;
} }
.tab.primary { .tab.primary {
flex: 0 0 auto; flex: 0 0 auto;
min-width: 5.625rem; min-width: 5.625rem;
margin: 0 0.5625rem; margin: 0 0.5625rem;
padding: 0.5625rem 1rem; padding: 0.5625rem 1rem;
background: linear-gradient(135deg, #FFE69A 0%, #F4CF62 45%, #D7A72E 100%); background: linear-gradient(135deg, #FFE69A 0%, #F4CF62 45%, #D7A72E 100%);
color: #493c1b; color: #493c1b;
border-radius: 31.21875rem; border-radius: 31.21875rem;
font-size: 0.9375rem; font-size: 0.9375rem;
font-weight: 800; font-weight: 800;
box-shadow: 0 0.3125rem 0.6875rem rgba(215,167,46,0.25), 0 0 0 0.0625rem rgba(255,255,255,0.70) inset; box-shadow: 0 0.3125rem 0.6875rem rgba(215, 167, 46, 0.25), 0 0 0 0.0625rem rgba(255, 255, 255, 0.7) inset;
} }

View File

@@ -1,21 +1,75 @@
/**
.about { padding: 0.75rem; * 这里是uni-app内置的常用样式变量
*
* uni-app 官方扩展插件及插件市场https://ext.dcloud.net.cn上很多三方插件均使用了这些样式变量
* 如果你是插件开发者建议你使用scss预处理并在插件代码中直接使用这些变量无需 import 这个文件方便用户通过搭积木的方式开发整体风格一致的App
*
*/
/**
* 如果你是App开发者插件使用者你可以通过修改这些变量来定制自己的插件主题实现自定义主题功能
*
* 如果你的项目同样使用了scss预处理你也可以直接在你的 scss 代码中使用如下变量,同时无需 import 这个文件
*/
/* 颜色变量 */
/* 行为相关颜色 */
/* 藏青系主色(高亮) */
/* 文字基本颜色 */
/* 背景颜色 */
/* 边框颜色 */
/* 尺寸变量 */
/* 文字尺寸 */
/* 图片尺寸 */
/* Border Radius */
/* 水平间距 */
/* 垂直间距 */
/* 透明度 */
/* 文章场景相关 */
/* 表单控件尺寸(统一配置,避免页面内硬编码) */
.about {
padding: 0.75rem;
} }
.hero { padding: 1rem 0.75rem; display: flex; flex-direction: column; align-items: center; gap: 0.3125rem; .hero {
padding: 1rem 0.75rem;
display: flex;
flex-direction: column;
align-items: center;
gap: 0.3125rem;
} }
.logo { width: 5rem; height: 5rem; border-radius: 1rem; .logo {
width: 5rem;
height: 5rem;
border-radius: 1rem;
} }
.title { margin-top: 0.25rem; font-size: 1.125rem; font-weight: 800; color: #333; .title {
margin-top: 0.25rem;
font-size: 1.125rem;
font-weight: 800;
color: #111;
} }
.subtitle { font-size: 0.8125rem; color: #888; .subtitle {
font-size: 0.8125rem;
color: #444;
} }
.card { margin-top: 0.5625rem; background: #fff; border-radius: 0.5rem; overflow: hidden; .card {
margin-top: 0.5625rem;
background: #ffffff;
border-radius: 0.5rem;
overflow: hidden;
} }
.row { display: flex; align-items: center; padding: 0.75rem; border-top: 0.03125rem solid #f2f2f2; .row {
display: flex;
align-items: center;
padding: 0.75rem;
border-top: 0.03125rem solid #e5e7eb;
} }
.label { color: #666; .label {
color: #444;
} }
.value { margin-left: auto; color: #333; .value {
} margin-left: auto;
.link { margin-left: auto; color: #1aad19; color: #111;
} }
.link {
margin-left: auto;
color: #4C8DFF;
}

View File

@@ -1,27 +1,254 @@
/**
* 这里是uni-app内置的常用样式变量
*
* uni-app 官方扩展插件及插件市场https://ext.dcloud.net.cn上很多三方插件均使用了这些样式变量
* 如果你是插件开发者建议你使用scss预处理并在插件代码中直接使用这些变量无需 import 这个文件方便用户通过搭积木的方式开发整体风格一致的App
*
*/
/**
* 如果你是App开发者插件使用者你可以通过修改这些变量来定制自己的插件主题实现自定义主题功能
*
* 如果你的项目同样使用了scss预处理你也可以直接在你的 scss 代码中使用如下变量,同时无需 import 这个文件
*/
/* 颜色变量 */
/* 行为相关颜色 */
/* 藏青系主色(高亮) */
/* 文字基本颜色 */
/* 背景颜色 */
/* 边框颜色 */
/* 尺寸变量 */
/* 文字尺寸 */
/* 图片尺寸 */
/* Border Radius */
/* 水平间距 */
/* 垂直间距 */
/* 透明度 */
/* 文章场景相关 */
/* 表单控件尺寸(统一配置,避免页面内硬编码) */
.me {
padding: 0.75rem;
}
.card.login {
display: flex;
flex-direction: column;
gap: 0.5rem;
padding: 0.6875rem;
background: #ffffff;
border-radius: 0.5rem;
margin-bottom: 0.75rem;
}
.login-title {
font-size: 0.875rem;
font-weight: 700;
}
.login-btn.minor {
background: #f1f1f1;
color: #111;
}
.hint {
font-size: 0.6875rem;
color: #444;
}
.card.user {
display: flex;
gap: 0.5625rem;
padding: 0.6875rem;
background: #ffffff;
border-radius: 0.5rem;
box-shadow: 0 0.1875rem 0.5rem rgba(0, 0, 0, 0.16);
align-items: center;
}
.card.user.guest {
justify-content: space-between;
}
.card.user.guest .meta {
flex: 1;
}
.card.user.guest .login-entry {
padding: 0.375rem 0.9375rem;
border-radius: 31.21875rem;
background: #4C8DFF;
color: #fff;
font-size: 0.875rem;
font-weight: 600;
}
.avatar {
width: 3.75rem;
height: 3.75rem;
border-radius: 1.875rem;
background: #f1f1f1;
}
.meta {
display: flex;
flex-direction: column;
gap: 0.1875rem;
}
.name {
font-size: 1.0625rem;
font-weight: 700;
color: #111;
}
.phone {
font-size: 0.8125rem;
color: #444;
}
.role {
font-size: 0.6875rem;
color: #444;
}
.me { padding: 0.75rem; /* VIP 卡片样式 */
.card.vip {
margin-top: 0.75rem;
padding: 0.6875rem;
background: #ffffff;
border-radius: 0.5rem;
box-shadow: 0 0.1875rem 0.5rem rgba(0, 0, 0, 0.12);
} }
.card.user { display: flex; gap: 0.5625rem; padding: 0.6875rem; background: #fff; border-radius: 0.5rem; box-shadow: 0 0.1875rem 0.5rem rgba(0,0,0,0.06); align-items: center; .card.vip.active {
border: 0.03125rem solid rgba(255, 208, 0, 0.6);
background-image: radial-gradient(60% 60% at 80% 0%, rgba(255, 214, 0, 0.08), transparent 60%);
} }
.avatar { width: 3.75rem; height: 3.75rem; border-radius: 1.875rem; background: #f5f5f5; .vip-row {
display: flex;
align-items: center;
gap: 0.375rem;
margin-bottom: 0.3125rem;
} }
.meta { display: flex; flex-direction: column; gap: 0.1875rem; .vip-badge {
background: #f1c40f;
color: #111;
font-weight: 800;
padding: 0.0625rem 0.3125rem;
border-radius: 0.25rem;
font-size: 0.6875rem;
} }
.name { font-size: 1.0625rem; font-weight: 700; color: #333; .vip-title {
font-size: 0.875rem;
font-weight: 700;
color: #111;
} }
.phone { font-size: 0.8125rem; color: #888; .vip-meta {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.25rem 0.5rem;
} }
.role { font-size: 0.6875rem; color: #999; .vip-meta .item {
display: flex;
align-items: center;
gap: 0.3125rem;
} }
.group { margin-top: 0.75rem; background: #fff; border-radius: 0.5rem; overflow: hidden; .vip-meta .label {
width: 2.5rem;
color: #444;
font-size: 0.75rem;
} }
.group-title { padding: 0.5625rem 0.6875rem; font-size: 0.8125rem; color: #999; background: #fafafa; .vip-meta .value {
color: #111;
font-size: 0.8125rem;
word-break: break-all;
} }
.cell { display: flex; align-items: center; padding: 0.8125rem 0.6875rem; border-top: 0.03125rem solid #f0f0f0; color: #333; .group {
margin-top: 0.75rem;
background: #ffffff;
border-radius: 0.5rem;
overflow: hidden;
} }
.cell .desc { margin-left: auto; margin-right: 0.25rem; font-size: 0.6875rem; color: #999; .group-title {
padding: 0.5625rem 0.6875rem;
font-size: 0.8125rem;
color: #444;
background: #f1f1f1;
} }
.cell .arrow { margin-left: auto; color: #bbb; .cell {
display: flex;
align-items: center;
padding: 0.8125rem 0.6875rem;
border-top: 0.03125rem solid #e5e7eb;
color: #111;
gap: 0.5625rem;
} }
.cell.danger { color: #dd524d; justify-content: center; font-weight: 700; .cell-left {
display: flex;
align-items: center;
gap: 0.4375rem;
} }
.vip-tag {
padding: 0.125rem 0.375rem;
border-radius: 31.21875rem;
background: rgba(76, 141, 255, 0.15);
color: #4C8DFF;
font-size: 0.6875rem;
}
.vip-tag.pending {
background: rgba(76, 141, 255, 0.06);
color: #99a2b3;
}
.cell .desc {
margin-left: auto;
margin-right: 0.25rem;
font-size: 0.6875rem;
color: #444;
}
.cell .arrow {
margin-left: auto;
color: #99a2b3;
}
.cell.danger {
color: #dd524d;
justify-content: center;
font-weight: 700;
}
/* 简易对话框样式 */
.dialog-mask {
position: fixed;
left: 0;
right: 0;
top: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.45);
display: flex;
align-items: center;
justify-content: center;
z-index: 999;
}
.dialog {
width: 18.75rem;
background: #fff;
border-radius: 0.5rem;
padding: 0.75rem;
box-shadow: 0 0.25rem 0.75rem rgba(0, 0, 0, 0.2);
}
.dialog-title {
font-size: 0.9375rem;
font-weight: 700;
margin-bottom: 0.5rem;
}
.dialog-input {
width: 100%;
height: 2.25rem;
padding: 0 0.5rem;
border: 0.03125rem solid #e5e7eb;
border-radius: 0.3125rem;
background: #fff;
color: #111;
}
.dialog-actions {
display: flex;
gap: 0.5rem;
margin-top: 0.5625rem;
justify-content: flex-end;
}
.dialog-btn {
padding: 0.5rem 0.6875rem;
border-radius: 0.3125rem;
}
.dialog-btn.cancel {
background: #f1f1f1;
color: #111;
}
.dialog-btn.confirm {
background: #2979ff;
color: #fff;
}

View File

@@ -0,0 +1,73 @@
/**
* 这里是uni-app内置的常用样式变量
*
* uni-app 官方扩展插件及插件市场https://ext.dcloud.net.cn上很多三方插件均使用了这些样式变量
* 如果你是插件开发者建议你使用scss预处理并在插件代码中直接使用这些变量无需 import 这个文件方便用户通过搭积木的方式开发整体风格一致的App
*
*/
/**
* 如果你是App开发者插件使用者你可以通过修改这些变量来定制自己的插件主题实现自定义主题功能
*
* 如果你的项目同样使用了scss预处理你也可以直接在你的 scss 代码中使用如下变量,同时无需 import 这个文件
*/
/* 颜色变量 */
/* 行为相关颜色 */
/* 藏青系主色(高亮) */
/* 文字基本颜色 */
/* 背景颜色 */
/* 边框颜色 */
/* 尺寸变量 */
/* 文字尺寸 */
/* 图片尺寸 */
/* Border Radius */
/* 水平间距 */
/* 垂直间距 */
/* 透明度 */
/* 文章场景相关 */
/* 表单控件尺寸(统一配置,避免页面内硬编码) */
.orders {
padding: 0.5rem 0.5rem calc(env(safe-area-inset-bottom) + 0.5rem);
}
.hint {
color: #444;
padding: 0.75rem;
text-align: center;
}
.item {
background: #fff;
border: 0.03125rem solid #e5e7eb;
border-radius: 0.5rem;
padding: 0.5625rem;
margin: 0.375rem 0;
}
.row1 {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.1875rem;
}
.price {
color: #111;
font-weight: 800;
font-size: 1.0625rem;
}
.channel {
color: #666;
font-size: 0.75rem;
}
.row2 {
display: flex;
justify-content: space-between;
color: #666;
font-size: 0.75rem;
}
.row3 {
margin-top: 0.1875rem;
color: #4C8DFF;
font-size: 0.75rem;
}
.empty {
text-align: center;
color: #999;
padding: 1.25rem 0;
}

View File

@@ -0,0 +1,99 @@
/**
* 这里是uni-app内置的常用样式变量
*
* uni-app 官方扩展插件及插件市场https://ext.dcloud.net.cn上很多三方插件均使用了这些样式变量
* 如果你是插件开发者建议你使用scss预处理并在插件代码中直接使用这些变量无需 import 这个文件方便用户通过搭积木的方式开发整体风格一致的App
*
*/
/**
* 如果你是App开发者插件使用者你可以通过修改这些变量来定制自己的插件主题实现自定义主题功能
*
* 如果你的项目同样使用了scss预处理你也可以直接在你的 scss 代码中使用如下变量,同时无需 import 这个文件
*/
/* 颜色变量 */
/* 行为相关颜色 */
/* 藏青系主色(高亮) */
/* 文字基本颜色 */
/* 背景颜色 */
/* 边框颜色 */
/* 尺寸变量 */
/* 文字尺寸 */
/* 图片尺寸 */
/* Border Radius */
/* 水平间距 */
/* 垂直间距 */
/* 透明度 */
/* 文章场景相关 */
/* 表单控件尺寸(统一配置,避免页面内硬编码) */
.security {
padding: 0.75rem;
}
.card {
background: #ffffff;
border-radius: 0.5rem;
padding: 0.6875rem;
margin-bottom: 0.75rem;
box-shadow: 0 0.1875rem 0.5rem rgba(0, 0, 0, 0.08);
}
.cell {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.625rem 0;
border-bottom: 0.03125rem solid #e5e7eb;
}
.cell:last-of-type {
border-bottom: none;
}
.cell-label {
flex: 1;
font-size: 0.875rem;
color: #111;
}
.cell-input {
flex: 2;
height: 2.25rem;
padding: 0 0.5rem;
border: 0.03125rem solid #e5e7eb;
border-radius: 0.3125rem;
background: #fff;
color: #111;
}
.avatar-preview {
width: 3.125rem;
height: 3.125rem;
border-radius: 0.5rem;
background: #f1f1f1;
}
.arrow {
margin-left: 0.375rem;
color: #99a2b3;
font-size: 1rem;
}
.row {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.5rem;
}
.label {
width: 5rem;
color: #111;
font-size: 0.875rem;
}
.input {
flex: 1;
height: 2.25rem;
padding: 0 0.5rem;
border: 0.03125rem solid #e5e7eb;
border-radius: 0.3125rem;
background: #fff;
color: #111;
}
.btn {
margin-top: 0.25rem;
}
.btn.minor {
background: #f1f1f1;
color: #111;
}

View File

@@ -0,0 +1,289 @@
/**
* 这里是uni-app内置的常用样式变量
*
* uni-app 官方扩展插件及插件市场https://ext.dcloud.net.cn上很多三方插件均使用了这些样式变量
* 如果你是插件开发者建议你使用scss预处理并在插件代码中直接使用这些变量无需 import 这个文件方便用户通过搭积木的方式开发整体风格一致的App
*
*/
/**
* 如果你是App开发者插件使用者你可以通过修改这些变量来定制自己的插件主题实现自定义主题功能
*
* 如果你的项目同样使用了scss预处理你也可以直接在你的 scss 代码中使用如下变量,同时无需 import 这个文件
*/
/* 颜色变量 */
/* 行为相关颜色 */
/* 藏青系主色(高亮) */
/* 文字基本颜色 */
/* 背景颜色 */
/* 边框颜色 */
/* 尺寸变量 */
/* 文字尺寸 */
/* 图片尺寸 */
/* Border Radius */
/* 水平间距 */
/* 垂直间距 */
/* 透明度 */
/* 文章场景相关 */
/* 表单控件尺寸(统一配置,避免页面内硬编码) */
body {
background: linear-gradient(180deg, #f8fbff 0%, #ffffff 60%) !important;
}
.vip-page {
min-height: 100vh;
padding: 1rem 0.75rem 3.75rem;
box-sizing: border-box;
display: flex;
flex-direction: column;
gap: 0.875rem;
}
.vip-hero {
display: flex;
align-items: center;
gap: 0.625rem;
padding: 0.8125rem 0.875rem;
border-radius: 0.75rem;
background: rgba(255, 255, 255, 0.98);
border: 0.0625rem solid #edf2f9;
box-shadow: 0 0.3125rem 0.9375rem rgba(76, 141, 255, 0.12);
}
.hero-icon {
width: 2.75rem;
height: 2.75rem;
border-radius: 0.75rem;
background: #f0f6ff;
padding: 0.375rem;
}
.hero-text {
flex: 1;
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.hero-title {
font-size: 1.125rem;
font-weight: 800;
color: #4C8DFF;
letter-spacing: 0.03125rem;
}
.hero-subtitle {
font-size: 0.8125rem;
color: #5175b5;
line-height: 1.125rem;
}
.status-pill {
flex: 0 0 auto;
padding: 0.375rem 0.625rem;
border-radius: 31.21875rem;
background: #e6edfb;
color: #4463a6;
font-size: 0.75rem;
font-weight: 700;
border: 0.0625rem solid rgba(76, 141, 255, 0.2);
}
.status-pill.active {
background: #4c8dff;
color: #fff;
border-color: #4c8dff;
}
/* 指定 hero 内激活态徽标文本为黑色 */
.vip-hero .status-pill.active uni-text {
color: #000 !important;
}
.vip-summary {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 0.5rem;
background: rgba(255, 255, 255, 0.98);
padding: 0.75rem;
border-radius: 0.75rem;
border: 0.0625rem solid #eef3fb;
box-shadow: 0 0.25rem 0.75rem rgba(99, 132, 191, 0.1);
}
.summary-item {
background: #f6f9ff;
border-radius: 0.5625rem;
padding: 0.6875rem 0.75rem;
display: flex;
flex-direction: column;
gap: 0.375rem;
border: 0.0625rem solid rgba(76, 141, 255, 0.12);
}
.summary-label {
font-size: 0.75rem;
color: #5f7394;
}
.summary-value {
font-size: 0.9375rem;
font-weight: 700;
color: #1f2c3d;
}
.summary-value.success {
color: #1ead91;
}
.summary-value.highlight {
color: #2f58d1;
}
.benefit-section {
background: rgba(255, 255, 255, 0.98);
border-radius: 0.75rem;
padding: 0.875rem;
border: 0.0625rem solid #edf2f9;
box-shadow: 0 0.375rem 0.875rem rgba(32, 75, 143, 0.1);
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.section-header {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.section-title {
font-size: 1.0625rem;
font-weight: 800;
color: #111;
}
.section-subtitle {
font-size: 0.75rem;
color: #5f7394;
line-height: 1.0625rem;
}
.benefit-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 0.625rem;
}
.benefit-card {
background: #f7faff;
border-radius: 0.625rem;
padding: 0.75rem 0.625rem;
border: 0.0625rem solid rgba(76, 141, 255, 0.12);
box-shadow: 0 0.25rem 0.625rem rgba(0, 0, 0, 0.04);
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 0.4375rem;
}
.benefit-icon {
width: 1.5rem;
height: 1.5rem;
}
.benefit-title {
font-size: 0.875rem;
font-weight: 700;
color: #111;
}
.benefit-desc {
font-size: 0.75rem;
line-height: 1.0625rem;
color: #5f7394;
}
.purchase-card {
margin-top: auto;
background: linear-gradient(135deg, rgba(76, 141, 255, 0.14) 0%, rgba(76, 141, 255, 0.06) 100%);
border-radius: 0.875rem;
padding: 0.9375rem 0.875rem;
display: flex;
align-items: center;
gap: 0.75rem;
border: 0.0625rem solid rgba(76, 141, 255, 0.18);
box-shadow: 0 0.3125rem 0.75rem rgba(76, 141, 255, 0.15);
}
.purchase-text {
flex: 1;
display: flex;
flex-direction: column;
gap: 0.3125rem;
}
.purchase-title {
font-size: 1rem;
font-weight: 800;
color: #4C8DFF;
}
.purchase-desc {
font-size: 0.75rem;
color: #4463a6;
line-height: 1.0625rem;
}
.purchase-btn {
flex: 0 0 auto;
padding: 0.625rem 1.125rem;
border-radius: 31.21875rem;
border: none;
background: linear-gradient(135deg, #4788ff 0%, #2d6be6 100%);
color: #fff;
font-size: 0.875rem;
font-weight: 700;
box-shadow: 0 0.3125rem 0.6875rem rgba(45, 107, 230, 0.2);
}
.purchase-btn:active {
opacity: 0.88;
}
.apply-card {
margin-top: 0;
background: linear-gradient(135deg, rgba(30, 173, 145, 0.14) 0%, rgba(30, 173, 145, 0.06) 100%);
border-radius: 0.875rem;
padding: 0.9375rem 0.875rem;
display: flex;
align-items: center;
gap: 0.75rem;
border: 0.0625rem solid rgba(30, 173, 145, 0.18);
box-shadow: 0 0.3125rem 0.75rem rgba(30, 173, 145, 0.15);
}
.apply-text {
flex: 1;
display: flex;
flex-direction: column;
gap: 0.3125rem;
}
.apply-title {
font-size: 1rem;
font-weight: 800;
color: #1ead91;
}
.apply-desc {
font-size: 0.75rem;
color: #247a66;
line-height: 1.0625rem;
}
.apply-btn {
flex: 0 0 auto;
padding: 0.625rem 1.125rem;
border-radius: 31.21875rem;
border: none;
background-color: transparent;
background: linear-gradient(135deg, #1ead91 0%, #159b7e 100%);
color: #fff;
font-size: 0.875rem;
font-weight: 700;
box-shadow: 0 0.3125rem 0.6875rem rgba(21, 155, 126, 0.2);
}
.apply-btn::after {
border: none;
}
.apply-btn:active {
opacity: 0.9;
}
.apply-btn.disabled {
opacity: 0.5;
background: #c7e8df;
color: #fff;
box-shadow: none;
pointer-events: none;
}
@media (max-width: 375px) {
.vip-summary {
grid-template-columns: 1fr;
}
.benefit-grid {
grid-template-columns: 1fr;
}
.purchase-card {
flex-direction: column;
align-items: stretch;
}
.status-pill {
display: none;
}
}

View File

@@ -1,59 +1,281 @@
/**
* 这里是uni-app内置的常用样式变量
*
* uni-app 官方扩展插件及插件市场https://ext.dcloud.net.cn上很多三方插件均使用了这些样式变量
* 如果你是插件开发者建议你使用scss预处理并在插件代码中直接使用这些变量无需 import 这个文件方便用户通过搭积木的方式开发整体风格一致的App
*
*/
/**
* 如果你是App开发者插件使用者你可以通过修改这些变量来定制自己的插件主题实现自定义主题功能
*
* 如果你的项目同样使用了scss预处理你也可以直接在你的 scss 代码中使用如下变量,同时无需 import 这个文件
*/
/* 颜色变量 */
/* 行为相关颜色 */
/* 藏青系主色(高亮) */
/* 文字基本颜色 */
/* 背景颜色 */
/* 边框颜色 */
/* 尺寸变量 */
/* 文字尺寸 */
/* 图片尺寸 */
/* Border Radius */
/* 水平间距 */
/* 垂直间距 */
/* 透明度 */
/* 文章场景相关 */
/* 表单控件尺寸(统一配置,避免页面内硬编码) */
.order {
padding-bottom: 4.375rem;
}
.tabs {
display: flex;
justify-content: space-around;
padding: 0.5rem 0.75rem;
}
.tabs uni-text {
color: #444;
}
.tabs uni-text.active {
color: #111;
font-weight: 700;
}
.order { padding-bottom: 4.375rem; /* 三段式胶囊切换 */
.seg3 {
display: flex;
gap: 0;
margin: 0.375rem 0.5rem;
padding: 0.1875rem;
background: #fff;
border-radius: 31.21875rem;
box-shadow: 0 0.125rem 0.375rem rgba(0, 0, 0, 0.04);
} }
.tabs { display: flex; justify-content: space-around; padding: 0.5rem 0.75rem; .seg3-item {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
gap: 0.25rem;
padding: 0.375rem 0;
color: #111;
border-radius: 31.21875rem;
transition: box-shadow 0.2s ease, background 0.2s ease;
} }
.tabs uni-text { color: #666;
/* 间隔通过内边距处理,避免空选择器 */
.seg3-item.active {
background: #fff;
color: #4C8DFF;
box-shadow: 0 0.09375rem 0.3125rem rgba(76, 141, 255, 0.16);
} }
.tabs uni-text.active { color: #333; font-weight: 700; .seg3-icon {
width: 0.875rem;
height: 0.875rem;
opacity: 0.9;
} }
.subtabs { display: flex; gap: 0.5rem; padding: 0 0.75rem 0.5rem; .field {
display: flex;
align-items: center;
justify-content: flex-start;
padding: 0.6875rem 0.75rem;
background: #f8faff;
gap: 0.5rem;
} }
.subbtn { padding: 0.3125rem 0.625rem; border-radius: 31.21875rem; background: #f4f4f4; color: #666; .label {
width: 5rem;
color: #444;
} }
.subbtn.active { background: #ffe69a; color: #3f320f; .value {
flex: 1;
color: #111;
text-align: right;
} }
.field { display:flex; justify-content: space-between; padding: 0.6875rem 0.75rem; background: #fff; border-bottom: 0.03125rem solid #eee;
/* 汇总卡片:白底卡片+主色按钮 */
.summary {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.5625rem 0.625rem;
margin: 0.5rem 0.5625rem 0.3125rem;
background: none;
border-radius: 0.5625rem;
color: #111;
} }
.label { color:#666;
/* 加号改为图标按钮 */
.add {
margin: 0.75rem auto 0.5625rem;
width: 3.75rem;
height: 3.75rem;
border-radius: 0.875rem;
background: none;
border: 0;
color: #4C8DFF;
font-size: 2.25rem;
display: flex;
align-items: center;
justify-content: center;
box-shadow: none;
} }
.value { color:#333; .empty {
display: flex;
flex-direction: column;
align-items: center;
padding: 1.875rem 0;
color: #444;
} }
.summary { display:flex; justify-content: space-between; padding: 0.6875rem 0.75rem; color:#333; .empty-img {
width: 5rem;
margin-bottom: 0.5rem;
} }
.add { margin: 0.75rem auto; width: 3.75rem; height: 3.75rem; border-radius: 0.625rem; background: #c7eef7; color:#16a1c4; font-size: 2.25rem; display:flex; align-items:center; justify-content:center; .empty-text {
margin-bottom: 0.25rem;
} }
.empty { display:flex; flex-direction: column; align-items:center; padding: 1.875rem 0; color:#888; .list {
background: #fff;
margin: 0 0.5625rem 0.625rem;
border-radius: 0.5625rem;
overflow: hidden;
box-shadow: 0 0.25rem 0.75rem rgba(0, 0, 0, 0.06);
} }
.empty-img { width: 6.875rem; margin-bottom: 0.625rem; .row {
display: grid;
grid-template-columns: 1.5fr 1fr 1fr 1fr;
gap: 0.375rem;
padding: 0.5625rem 0.5rem;
align-items: center;
} }
.empty-text { margin-bottom: 0.25rem; .col.name {
padding-left: 0.375rem;
} }
.list { background:#fff; .col.amount {
text-align: right;
padding-right: 0.375rem;
color: #111;
} }
.row { display:grid; grid-template-columns: 1.5fr 1fr 1fr 1fr; gap: 0.375rem; padding: 0.5rem 0.375rem; align-items:center; border-bottom: 0.03125rem solid #f3f3f3; .bottom {
position: fixed;
left: 0;
right: 0;
bottom: 0;
background: #ffffff;
padding: 0.1875rem 0.5625rem calc(env(safe-area-inset-bottom) + 0.0625rem);
box-shadow: 0 -0.125rem 0.375rem rgba(0, 0, 0, 0.16);
} }
.col.name { padding-left: 0.375rem; .order .bottom uni-button {
margin: 0;
} }
.col.amount { text-align:right; padding-right: 0.375rem; color:#333;
/* 仅限开单页底部按钮样式(缩小高度) */
.order .bottom .primary {
width: 100%;
background: #4C8DFF;
color: #fff;
border-radius: 31.21875rem;
padding: 0.4375rem 0;
font-weight: 700;
font-size: 0.875rem;
} }
.bottom { position: fixed; left:0; right:0; bottom:0; background:#fff; padding: 0.5rem 0.75rem calc(env(safe-area-inset-bottom) + 0.5rem); box-shadow: 0 -0.125rem 0.375rem rgba(0,0,0,0.06); .order .bottom .ghost {
background: transparent;
color: #4C8DFF;
border: 0;
border-radius: 31.21875rem;
padding: 0.375rem 0;
font-size: 0.875rem;
} }
.primary { width: 100%; background: linear-gradient(135deg, #FFE69A 0%, #F4CF62 45%, #D7A72E 100%); color:#493c1b; border-radius: 31.21875rem; padding: 0.625rem 0; font-weight:800;
/* 收款/付款页样式 */
.pay-row .pay-input {
text-align: right;
color: #111;
} }
/* 收款/付款页样式 */ .textarea {
.pay-row .pay-input { text-align: right; color:#333; position: relative;
padding: 0.5rem 0.75rem;
background: #f8faff;
} }
.textarea { position: relative; padding: 0.5rem 0.75rem; background:#fff; border-top: 0.03125rem solid #eee; .amount-badge {
position: absolute;
right: 0.75rem;
top: -1rem;
background: #4C8DFF;
color: #fff;
padding: 0.3125rem 0.625rem;
border-radius: 0.4375rem;
font-size: 0.75rem;
} }
.amount-badge { position: absolute; right: 0.75rem; top: -1.125rem; background: #d1f0ff; color:#107e9b; padding: 0.25rem 0.5rem; border-radius: 0.375rem; font-size: 0.75rem; .date-mini {
position: absolute;
right: 0.75rem;
bottom: 0.625rem;
color: #444;
font-size: 0.75rem;
} }
.date-mini { position: absolute; right: 0.75rem; bottom: 0.625rem; color:#666; font-size: 0.75rem;
/* 分类chips样式选中后文字变红 */
.chips {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 0.375rem 0.5rem;
padding: 0.375rem 0.75rem;
} }
/* 分类chips样式选中后文字变红 */ .chip {
.chips { display:flex; flex-wrap: wrap; gap: 0.375rem; padding: 0.375rem 0.75rem; padding: 0.375rem 0.625rem;
border-radius: 31.21875rem;
background: #f1f1f1;
color: #444;
text-align: center;
} }
.chip { padding: 0.3125rem 0.625rem; border-radius: 31.21875rem; background: #f4f4f4; color:#666; .chip.active {
background: rgba(76, 141, 255, 0.15);
color: #4C8DFF;
} }
.chip.active { color: #e54d42;
/* 顶部业务 Tabs 显示 */
/* 快捷操作宫格 */
/* 信息卡片式表达(更稳重) */
.info-card {
display: grid;
grid-template-columns: 1fr 1fr auto;
gap: 0.3125rem;
margin: 0.3125rem 0.375rem 0;
background: transparent;
padding: 0;
align-items: center;
} }
.info-field {
background: #fff;
border: 0;
border-radius: 0.4375rem;
padding: 0.375rem 0.4375rem;
box-shadow: 0 0.1875rem 0.5rem rgba(0, 0, 0, 0.06);
}
.info-label {
color: #444;
font-size: 0.75rem;
margin-right: 0.25rem;
}
.info-value {
color: #111;
font-weight: 700;
}
/* 缩小“加商品”按钮尺寸,仅在本页卡片内 */
.order .info-card .info-action {
display: flex;
align-items: center;
gap: 0.1875rem;
background: #4C8DFF;
color: #fff;
border-radius: 0.375rem;
padding: 0.25rem 0.375rem;
box-shadow: 0 0.15625rem 0.375rem rgba(76, 141, 255, 0.18);
font-size: 0.8125rem;
}
.order .info-card .info-icon {
width: 0.75rem;
height: 0.75rem;
}

View File

@@ -9,25 +9,169 @@
} }
.thumb[data-v-7bd1ddd2] { width: 100%; height: 100%; .thumb[data-v-7bd1ddd2] { width: 100%; height: 100%;
} }
.remove[data-v-7bd1ddd2] { position: absolute; right: 0.1875rem; top: 0.1875rem; background: rgba(0,0,0,0.45); color: #fff; width: 1.25rem; height: 1.25rem; text-align: center; line-height: 1.25rem; border-radius: 0.625rem; font-size: 0.875rem; .remove[data-v-7bd1ddd2] { position: absolute; right: 0.1875rem; top: 0.1875rem; width: 1.3125rem; height: 1.3125rem;
} }
.adder[data-v-7bd1ddd2] { width: 6.5625rem; height: 6.5625rem; border: 0.0625rem dashed #ccc; border-radius: 0.375rem; display: flex; align-items: center; justify-content: center; color: #999; position: absolute; left: 0; top: 0; .adder[data-v-7bd1ddd2] { width: 6.5625rem; height: 6.5625rem; border: 0.0625rem dashed #ccc; border-radius: 0.375rem; display: flex; align-items: center; justify-content: center; color: #999; position: absolute; left: 0; top: 0;
} }
/**
.page { background:#f6f6f6; height: 100vh; * 这里是uni-app内置的常用样式变量
*
* uni-app 官方扩展插件及插件市场https://ext.dcloud.net.cn上很多三方插件均使用了这些样式变量
* 如果你是插件开发者建议你使用scss预处理并在插件代码中直接使用这些变量无需 import 这个文件方便用户通过搭积木的方式开发整体风格一致的App
*
*/
/**
* 如果你是App开发者插件使用者你可以通过修改这些变量来定制自己的插件主题实现自定义主题功能
*
* 如果你的项目同样使用了scss预处理你也可以直接在你的 scss 代码中使用如下变量,同时无需 import 这个文件
*/
/* 颜色变量 */
/* 行为相关颜色 */
/* 藏青系主色(高亮) */
/* 文字基本颜色 */
/* 背景颜色 */
/* 边框颜色 */
/* 尺寸变量 */
/* 文字尺寸 */
/* 图片尺寸 */
/* Border Radius */
/* 水平间距 */
/* 垂直间距 */
/* 透明度 */
/* 文章场景相关 */
/* 表单控件尺寸(统一配置,避免页面内硬编码) */
.page {
background: #ffffff;
min-height: 100vh;
padding-bottom: 5rem;
box-sizing: border-box;
} }
.card { background:#fff; margin: 0.5rem; padding: 0.5rem; border-radius: 0.375rem; .hero.small {
margin: 0.6875rem 0.75rem 0.375rem;
padding: 0 0.125rem 0.5625rem;
color: #111;
border-bottom: 0.0625rem solid rgba(94, 124, 174, 0.12);
} }
.row { display:flex; gap: 0.375rem; align-items: center; margin-bottom: 0.375rem; .hero.small .title {
font-size: 1.0625rem;
font-weight: 800;
} }
.label { width: 5.625rem; color:#666; .hero.small .sub {
display: block;
margin-top: 0.1875rem;
color: #444;
font-size: 0.75rem;
} }
.row uni-input { flex:1; background:#f7f7f7; border-radius: 0.3125rem; padding: 0.375rem; .section {
margin: 0 0.75rem 0.875rem;
padding-bottom: 0.1875rem;
border-bottom: 0.0625rem solid rgba(94, 124, 174, 0.1);
} }
.picker { padding: 0.25rem 0.375rem; background:#f0f0f0; border-radius: 0.3125rem; color:#666; margin-left: 0.25rem; .section:last-of-type {
border-bottom: 0;
margin-bottom: 0;
} }
.prices uni-input { width: 30%; .section .row:first-child .label {
font-weight: 700;
color: #111;
} }
.fixed { position: fixed; left: 0; right: 0; bottom: 0; background:#fff; padding: 0.375rem 0.5rem; display:flex; gap: 0.5rem; .row {
display: flex;
gap: 0.25rem;
align-items: center;
margin-top: 0.5625rem;
} }
.row .input-long {
flex: 1.2;
}
.row:first-child {
margin-top: 0;
}
.label {
width: 4.6875rem;
color: #444;
font-size: 0.8125rem;
}
.row uni-input {
flex: 1;
background: #f7f9fc;
border-radius: 0.4375rem;
padding: 0.5625rem 0.625rem;
color: #111;
border: 0;
box-shadow: inset 0 0 0 0.0625rem rgba(134, 155, 191, 0.06);
}
.picker-btn {
background: #ffffff;
border: 0.0625rem solid rgba(76, 141, 255, 0.45);
color: #4C8DFF;
padding: 0 0.75rem;
border-radius: 31.21875rem;
font-size: 0.75rem;
}
.picker {
padding: 0.5rem 0.6875rem;
background: #f7f9fc;
border-radius: 0.4375rem;
color: #444;
margin-left: 0.25rem;
border: 0;
box-shadow: inset 0 0 0 0.0625rem rgba(134, 155, 191, 0.06);
}
.prices uni-input {
width: 30%;
}
.section uni-textarea {
width: 100%;
min-height: 5rem;
background: #f7f9fc;
border-radius: 0.4375rem;
padding: 0.625rem 0.6875rem;
box-sizing: border-box;
color: #111;
border: 0;
box-shadow: inset 0 0 0 0.0625rem rgba(134, 155, 191, 0.06);
}
.fixed {
position: fixed;
left: 0;
right: 0;
bottom: env(safe-area-inset-bottom);
background: #ffffff;
padding: 0.5rem 0.5rem calc(0.5rem + constant(safe-area-inset-bottom)) 0.5rem;
display: flex;
gap: 0.5rem;
box-shadow: 0 -0.1875rem 0.5625rem rgba(24, 55, 105, 0.08);
z-index: 999;
}
.fixed .primary {
flex: 1;
background: #4C8DFF;
color: #fff;
border-radius: 31.21875rem;
padding: 0.5625rem 0;
font-weight: 700;
}
.fixed .ghost {
flex: 1;
background: #ffffff;
color: #4C8DFF;
border: 0.0625rem solid rgba(76, 141, 255, 0.45);
border-radius: 31.21875rem;
padding: 0.5625rem 0;
}
.tip {
margin: 0 0.9375rem 0.625rem;
padding: 0.5rem 0.625rem;
border-radius: 0.5rem;
font-size: 0.75rem;
}
.tip.platform {
background: rgba(45, 140, 240, 0.12);
color: #2d8cf0;
}
.tip.custom {
background: rgba(103, 194, 58, 0.12);
color: #67c23a;
}

View File

@@ -1,33 +1,163 @@
/**
.page { display:flex; flex-direction: column; height: 100vh; * 这里是uni-app内置的常用样式变量
*
* uni-app 官方扩展插件及插件市场https://ext.dcloud.net.cn上很多三方插件均使用了这些样式变量
* 如果你是插件开发者建议你使用scss预处理并在插件代码中直接使用这些变量无需 import 这个文件方便用户通过搭积木的方式开发整体风格一致的App
*
*/
/**
* 如果你是App开发者插件使用者你可以通过修改这些变量来定制自己的插件主题实现自定义主题功能
*
* 如果你的项目同样使用了scss预处理你也可以直接在你的 scss 代码中使用如下变量,同时无需 import 这个文件
*/
/* 颜色变量 */
/* 行为相关颜色 */
/* 藏青系主色(高亮) */
/* 文字基本颜色 */
/* 背景颜色 */
/* 边框颜色 */
/* 尺寸变量 */
/* 文字尺寸 */
/* 图片尺寸 */
/* Border Radius */
/* 水平间距 */
/* 垂直间距 */
/* 透明度 */
/* 文章场景相关 */
/* 表单控件尺寸(统一配置,避免页面内硬编码) */
.page {
display: flex;
flex-direction: column;
height: 100vh;
} }
.tabs { display:flex; background:#fff; .tabs {
display: flex;
background: #ffffff;
} }
.tab { flex:1; text-align:center; padding: 0.625rem 0; color:#666; .tab {
flex: 1;
text-align: center;
padding: 0.625rem 0;
color: #444;
} }
.tab.active { color:#18b566; font-weight: 600; .tab.active {
color: #4C8DFF;
font-weight: 600;
} }
.search { display:flex; gap: 0.375rem; padding: 0.5rem; background:#fff; align-items: center; .tab.extra {
flex: 0 0 5.625rem;
color: #4C8DFF;
font-weight: 600;
} }
.search uni-input { flex:1; background:#f6f6f6; border-radius: 0.375rem; padding: 0.375rem; .search {
display: flex;
gap: 0.375rem;
padding: 0.5rem;
background: #ffffff;
align-items: center;
} }
.picker { padding: 0.25rem 0.375rem; background:#f0f0f0; border-radius: 0.3125rem; color:#666; .search uni-input {
flex: 1;
background: #f1f1f1;
border-radius: 0.375rem;
padding: 0.375rem;
color: #111;
} }
.list { flex:1; .picker {
padding: 0.25rem 0.375rem;
background: #f1f1f1;
border-radius: 0.3125rem;
color: #444;
} }
.item { display:flex; padding: 0.625rem; background:#fff; border-bottom: 0.03125rem solid #f1f1f1; .template-mode {
flex-direction: column;
align-items: stretch;
gap: 0.25rem;
} }
.thumb { width: 3.75rem; height: 3.75rem; border-radius: 0.375rem; margin-right: 0.5rem; background:#fafafa; .picker-row {
display: flex;
gap: 0.375rem;
} }
.content { flex:1; .params-wrap {
margin-top: 0.1875rem;
background: #ffffff;
border-radius: 0.375rem;
padding: 0.25rem 0.25rem;
} }
.name { color:#333; margin-bottom: 0.1875rem; font-weight: 600; .list {
flex: 1;
} }
.meta { color:#888; font-size: 0.75rem; .item {
display: flex;
padding: 0.625rem;
background: #ffffff;
border-bottom: 0.03125rem solid #e5e7eb;
} }
.price { margin-left: 0.625rem; color:#f60; .thumb {
width: 3.75rem;
height: 3.75rem;
border-radius: 0.375rem;
margin-right: 0.5rem;
background: #f1f1f1;
} }
.empty { height: 60vh; display:flex; align-items:center; justify-content:center; color:#999; .content {
flex: 1;
} }
.fab { position: fixed; right: 0.9375rem; bottom: 3.75rem; width: 3.125rem; height: 3.125rem; background:#18b566; color:#fff; border-radius: 1.5625rem; text-align:center; line-height: 3.125rem; font-size: 1.5rem; box-shadow: 0 0.25rem 0.625rem rgba(0,0,0,0.15); .name {
color: #111;
margin-bottom: 0.1875rem;
font-weight: 600;
display: flex;
align-items: center;
gap: 0.375rem;
} }
.tag-platform {
font-size: 0.6875rem;
color: #fff;
background: #2d8cf0;
padding: 0.125rem 0.3125rem;
border-radius: 0.25rem;
}
.tag-custom {
font-size: 0.6875rem;
color: #fff;
background: #67c23a;
padding: 0.125rem 0.3125rem;
border-radius: 0.25rem;
}
.tag-deleted {
font-size: 0.6875rem;
color: #fff;
background: #909399;
padding: 0.125rem 0.3125rem;
border-radius: 0.25rem;
}
.meta {
color: #444;
font-size: 0.75rem;
}
.price {
margin-left: 0.625rem;
color: #4C8DFF;
}
.empty {
height: 60vh;
display: flex;
align-items: center;
justify-content: center;
color: #444;
}
.fab {
position: fixed;
right: 0.9375rem;
bottom: 3.75rem;
width: 3.125rem;
height: 3.125rem;
background: #4C8DFF;
color: #fff;
border-radius: 1.5625rem;
text-align: center;
line-height: 3.125rem;
font-size: 1.5rem;
box-shadow: 0 0.25rem 0.625rem rgba(0, 0, 0, 0.15);
}

View File

@@ -0,0 +1,124 @@
/**
* 这里是uni-app内置的常用样式变量
*
* uni-app 官方扩展插件及插件市场https://ext.dcloud.net.cn上很多三方插件均使用了这些样式变量
* 如果你是插件开发者建议你使用scss预处理并在插件代码中直接使用这些变量无需 import 这个文件方便用户通过搭积木的方式开发整体风格一致的App
*
*/
/**
* 如果你是App开发者插件使用者你可以通过修改这些变量来定制自己的插件主题实现自定义主题功能
*
* 如果你的项目同样使用了scss预处理你也可以直接在你的 scss 代码中使用如下变量,同时无需 import 这个文件
*/
/* 颜色变量 */
/* 行为相关颜色 */
/* 藏青系主色(高亮) */
/* 文字基本颜色 */
/* 背景颜色 */
/* 边框颜色 */
/* 尺寸变量 */
/* 文字尺寸 */
/* 图片尺寸 */
/* Border Radius */
/* 水平间距 */
/* 垂直间距 */
/* 透明度 */
/* 文章场景相关 */
/* 表单控件尺寸(统一配置,避免页面内硬编码) */
.page {
padding: 0.75rem 0.75rem 5rem;
background: #f6f7fb;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.625rem;
}
.model {
font-size: 1.125rem;
font-weight: 700;
color: #2d3a4a;
}
.status.deleted {
font-size: 0.75rem;
padding: 0.1875rem 0.5625rem;
border-radius: 31.21875rem;
background: #c0c4cc;
color: #fff;
}
.section {
background: #fff;
border-radius: 0.5rem;
padding: 0.625rem 0.6875rem;
margin-bottom: 0.75rem;
box-shadow: 0 0.25rem 0.75rem rgba(0, 0, 0, 0.04);
}
.row {
display: flex;
justify-content: space-between;
padding: 0.375rem 0;
border-bottom: 0.03125rem solid #f1f2f5;
}
.row:last-child {
border-bottom: none;
}
.label {
width: 5rem;
font-size: 0.8125rem;
color: #7a8899;
}
.value {
flex: 1;
text-align: right;
font-size: 0.8125rem;
color: #2d3a4a;
word-break: break-all;
}
.block-title {
font-size: 0.875rem;
font-weight: 600;
color: #2d3a4a;
margin-bottom: 0.375rem;
}
.placeholder {
font-size: 0.8125rem;
color: #7a8899;
}
.params {
display: flex;
flex-direction: column;
gap: 0.375rem;
}
.param {
display: flex;
justify-content: space-between;
font-size: 0.8125rem;
color: #2d3a4a;
}
.param-key {
color: #7a8899;
}
.images {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0.375rem;
}
.image {
width: 100%;
height: 6.25rem;
border-radius: 0.5rem;
background: #f0f2f5;
}
.footer {
display: flex;
justify-content: flex-end;
gap: 0.625rem;
}
.loading {
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
color: #7a8899;
}

View File

@@ -1,15 +1,61 @@
/**
.page { display:flex; flex-direction: column; height: 100vh; * 这里是uni-app内置的常用样式变量
*
* uni-app 官方扩展插件及插件市场https://ext.dcloud.net.cn上很多三方插件均使用了这些样式变量
* 如果你是插件开发者建议你使用scss预处理并在插件代码中直接使用这些变量无需 import 这个文件方便用户通过搭积木的方式开发整体风格一致的App
*
*/
/**
* 如果你是App开发者插件使用者你可以通过修改这些变量来定制自己的插件主题实现自定义主题功能
*
* 如果你的项目同样使用了scss预处理你也可以直接在你的 scss 代码中使用如下变量,同时无需 import 这个文件
*/
/* 颜色变量 */
/* 行为相关颜色 */
/* 藏青系主色(高亮) */
/* 文字基本颜色 */
/* 背景颜色 */
/* 边框颜色 */
/* 尺寸变量 */
/* 文字尺寸 */
/* 图片尺寸 */
/* Border Radius */
/* 水平间距 */
/* 垂直间距 */
/* 透明度 */
/* 文章场景相关 */
/* 表单控件尺寸(统一配置,避免页面内硬编码) */
.page {
display: flex;
flex-direction: column;
height: 100vh;
} }
.search { display:flex; gap: 0.375rem; padding: 0.5rem; background:#fff; .search {
display: flex;
gap: 0.375rem;
padding: 0.5rem;
background: #ffffff;
} }
.search uni-input { flex:1; background:#f6f6f6; border-radius: 0.375rem; padding: 0.375rem; .search uni-input {
flex: 1;
background: #f1f1f1;
border-radius: 0.375rem;
padding: 0.375rem;
color: #111;
} }
.list { flex:1; .list {
flex: 1;
} }
.item { padding: 0.625rem 0.75rem; background:#fff; border-bottom: 0.03125rem solid #f1f1f1; .item {
padding: 0.625rem 0.75rem;
background: #ffffff;
border-bottom: 0.03125rem solid #e5e7eb;
} }
.name { color:#333; margin-bottom: 0.1875rem; .name {
} color: #111;
.meta { color:#888; font-size: 0.75rem; margin-bottom: 0.1875rem;
} }
.meta {
color: #444;
font-size: 0.75rem;
}

View File

@@ -0,0 +1,134 @@
/**
* 这里是uni-app内置的常用样式变量
*
* uni-app 官方扩展插件及插件市场https://ext.dcloud.net.cn上很多三方插件均使用了这些样式变量
* 如果你是插件开发者建议你使用scss预处理并在插件代码中直接使用这些变量无需 import 这个文件方便用户通过搭积木的方式开发整体风格一致的App
*
*/
/**
* 如果你是App开发者插件使用者你可以通过修改这些变量来定制自己的插件主题实现自定义主题功能
*
* 如果你的项目同样使用了scss预处理你也可以直接在你的 scss 代码中使用如下变量,同时无需 import 这个文件
*/
/* 颜色变量 */
/* 行为相关颜色 */
/* 藏青系主色(高亮) */
/* 文字基本颜色 */
/* 背景颜色 */
/* 边框颜色 */
/* 尺寸变量 */
/* 文字尺寸 */
/* 图片尺寸 */
/* Border Radius */
/* 水平间距 */
/* 垂直间距 */
/* 透明度 */
/* 文章场景相关 */
/* 表单控件尺寸(统一配置,避免页面内硬编码) */
.page {
padding: 0.75rem 0.75rem 5rem;
background: #f6f7fb;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.625rem;
}
.model {
font-size: 1.125rem;
font-weight: 700;
color: #2d3a4a;
}
.status {
font-size: 0.8125rem;
padding: 0.1875rem 0.5625rem;
border-radius: 31.21875rem;
}
.status.pending {
background: rgba(246, 190, 0, 0.15);
color: #c47f00;
}
.status.approved {
background: rgba(103, 194, 58, 0.15);
color: #409eff;
}
.status.rejected {
background: rgba(255, 87, 115, 0.18);
color: #f56c6c;
}
.section {
background: #fff;
border-radius: 0.5rem;
padding: 0.625rem 0.6875rem;
margin-bottom: 0.75rem;
box-shadow: 0 0.25rem 0.75rem rgba(0, 0, 0, 0.04);
}
.row {
display: flex;
justify-content: space-between;
padding: 0.375rem 0;
border-bottom: 0.03125rem solid #f1f2f5;
}
.row:last-child {
border-bottom: none;
}
.label {
width: 5rem;
font-size: 0.8125rem;
color: #7a8899;
}
.value {
flex: 1;
text-align: right;
font-size: 0.8125rem;
color: #2d3a4a;
word-break: break-all;
}
.block-title {
font-size: 0.875rem;
font-weight: 600;
color: #2d3a4a;
margin-bottom: 0.375rem;
}
.placeholder {
font-size: 0.8125rem;
color: #7a8899;
}
.params {
display: flex;
flex-direction: column;
gap: 0.375rem;
}
.param {
display: flex;
justify-content: space-between;
font-size: 0.8125rem;
color: #2d3a4a;
}
.param-key {
color: #7a8899;
}
.images {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0.375rem;
}
.image {
width: 100%;
height: 6.25rem;
border-radius: 0.5rem;
background: #f0f2f5;
}
.footer {
display: flex;
justify-content: flex-end;
gap: 0.625rem;
}
.loading {
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
color: #7a8899;
}

View File

@@ -0,0 +1,164 @@
/**
* 这里是uni-app内置的常用样式变量
*
* uni-app 官方扩展插件及插件市场https://ext.dcloud.net.cn上很多三方插件均使用了这些样式变量
* 如果你是插件开发者建议你使用scss预处理并在插件代码中直接使用这些变量无需 import 这个文件方便用户通过搭积木的方式开发整体风格一致的App
*
*/
/**
* 如果你是App开发者插件使用者你可以通过修改这些变量来定制自己的插件主题实现自定义主题功能
*
* 如果你的项目同样使用了scss预处理你也可以直接在你的 scss 代码中使用如下变量,同时无需 import 这个文件
*/
/* 颜色变量 */
/* 行为相关颜色 */
/* 藏青系主色(高亮) */
/* 文字基本颜色 */
/* 背景颜色 */
/* 边框颜色 */
/* 尺寸变量 */
/* 文字尺寸 */
/* 图片尺寸 */
/* Border Radius */
/* 水平间距 */
/* 垂直间距 */
/* 透明度 */
/* 文章场景相关 */
/* 表单控件尺寸(统一配置,避免页面内硬编码) */
.page {
display: flex;
flex-direction: column;
height: 100vh;
background: #f6f7fb;
padding-bottom: 4.375rem;
}
.hero {
padding: 0.75rem;
background: #fff;
box-shadow: 0 0.3125rem 0.9375rem rgba(0, 0, 0, 0.04);
}
.title {
font-size: 1.0625rem;
font-weight: 700;
color: #2d3a4a;
}
.desc {
font-size: 0.75rem;
color: #7a8899;
margin-top: 0.25rem;
}
.tabs {
display: flex;
background: #fff;
margin: 0.5rem;
border-radius: 31.21875rem;
overflow: hidden;
box-shadow: inset 0 0 0 0.03125rem rgba(76, 141, 255, 0.1);
}
.tab {
flex: 1;
text-align: center;
padding: 0.625rem 0;
font-size: 0.875rem;
color: #7a8899;
}
.tab.active {
background: linear-gradient(135deg, #4c8dff, #6ab7ff);
color: #fff;
font-weight: 600;
}
.list {
flex: 1;
padding: 0 0.625rem;
}
.cards {
display: flex;
flex-direction: column;
gap: 0.625rem;
padding-bottom: 1.25rem;
}
.card {
background: #fff;
border-radius: 0.5625rem;
padding: 0.6875rem;
box-shadow: 0 0.3125rem 0.9375rem rgba(0, 0, 0, 0.05);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.375rem;
}
.model {
font-size: 0.9375rem;
font-weight: 700;
color: #2d3a4a;
}
.status {
font-size: 0.75rem;
padding: 0.1875rem 0.5625rem;
border-radius: 31.21875rem;
}
.status.pending {
background: rgba(246, 190, 0, 0.15);
color: #c47f00;
}
.status.approved {
background: rgba(103, 194, 58, 0.15);
color: #409eff;
}
.status.rejected {
background: rgba(255, 87, 115, 0.18);
color: #f56c6c;
}
.card-body {
display: flex;
flex-direction: column;
gap: 0.1875rem;
color: #4f5969;
font-size: 0.8125rem;
}
.name {
font-weight: 600;
color: #2d3a4a;
}
.card-footer {
display: flex;
gap: 0.375rem;
margin-top: 0.5rem;
}
.empty {
height: 60vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: #8894a3;
gap: 0.625rem;
}
.empty .primary {
background: #4c8dff;
color: #fff;
border-radius: 31.21875rem;
padding: 0.375rem 0.9375rem;
}
.loading, .finished {
text-align: center;
padding: 0.625rem 0;
color: #7a8899;
}
.fab {
position: fixed;
right: 0.9375rem;
bottom: 3.75rem;
width: 3.125rem;
height: 3.125rem;
background: linear-gradient(135deg, #4c8dff, #6ab7ff);
color: #fff;
border-radius: 1.5625rem;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.5rem;
box-shadow: 0 0.625rem 1.25rem rgba(0, 0, 0, 0.2);
}

View File

@@ -0,0 +1,142 @@
.uploader[data-v-7bd1ddd2] { padding: 0.375rem; background: #fff;
}
.grid[data-v-7bd1ddd2] { position: relative;
}
.area[data-v-7bd1ddd2] { width: 100%; position: relative;
}
.cell[data-v-7bd1ddd2] { position: absolute; border-radius: 0.375rem; overflow: hidden; box-shadow: 0 0 0.03125rem rgba(0,0,0,0.08);
}
.thumb[data-v-7bd1ddd2] { width: 100%; height: 100%;
}
.remove[data-v-7bd1ddd2] { position: absolute; right: 0.1875rem; top: 0.1875rem; width: 1.3125rem; height: 1.3125rem;
}
.adder[data-v-7bd1ddd2] { width: 6.5625rem; height: 6.5625rem; border: 0.0625rem dashed #ccc; border-radius: 0.375rem; display: flex; align-items: center; justify-content: center; color: #999; position: absolute; left: 0; top: 0;
}
/**
* 这里是uni-app内置的常用样式变量
*
* uni-app 官方扩展插件及插件市场https://ext.dcloud.net.cn上很多三方插件均使用了这些样式变量
* 如果你是插件开发者建议你使用scss预处理并在插件代码中直接使用这些变量无需 import 这个文件方便用户通过搭积木的方式开发整体风格一致的App
*
*/
/**
* 如果你是App开发者插件使用者你可以通过修改这些变量来定制自己的插件主题实现自定义主题功能
*
* 如果你的项目同样使用了scss预处理你也可以直接在你的 scss 代码中使用如下变量,同时无需 import 这个文件
*/
/* 颜色变量 */
/* 行为相关颜色 */
/* 藏青系主色(高亮) */
/* 文字基本颜色 */
/* 背景颜色 */
/* 边框颜色 */
/* 尺寸变量 */
/* 文字尺寸 */
/* 图片尺寸 */
/* Border Radius */
/* 水平间距 */
/* 垂直间距 */
/* 透明度 */
/* 文章场景相关 */
/* 表单控件尺寸(统一配置,避免页面内硬编码) */
.page {
padding: 0.75rem 0.75rem 3.75rem;
background: #f6f7fb;
}
.hero {
padding: 0.75rem;
background: linear-gradient(135deg, #4c8dff, #6ab7ff);
border-radius: 0.625rem;
color: #fff;
margin-bottom: 0.75rem;
}
.title {
font-size: 1.125rem;
font-weight: 700;
}
.desc {
font-size: 0.8125rem;
margin-top: 0.25rem;
opacity: 0.9;
}
.section {
background: #fff;
border-radius: 0.5rem;
padding: 0.625rem 0.6875rem;
margin-bottom: 0.75rem;
box-shadow: 0 0.3125rem 0.9375rem rgba(0, 0, 0, 0.04);
}
.row {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 0;
border-bottom: 0.03125rem solid #f1f2f5;
}
.row:last-child {
border-bottom: none;
}
.row.required .label::after {
content: "*";
color: #ff5b5b;
margin-left: 0.1875rem;
}
.label {
width: 4.0625rem;
font-size: 0.875rem;
color: #2d3a4a;
}
uni-input {
flex: 1;
background: #f8f9fb;
border-radius: 0.375rem;
padding: 0.5rem 0.5625rem;
font-size: 0.875rem;
color: #222;
}
.textarea {
width: 100%;
min-height: 5rem;
background: #f8f9fb;
border-radius: 0.375rem;
padding: 0.5625rem;
font-size: 0.875rem;
color: #222;
}
.picker {
flex: 1;
background: #f8f9fb;
border-radius: 0.375rem;
padding: 0.5625rem;
font-size: 0.875rem;
color: #222;
}
.picker-btn {
background: #4c8dff;
color: #fff;
border-radius: 31.21875rem;
padding: 0.3125rem 0.6875rem;
}
.triple uni-input {
flex: 1;
}
.fixed {
position: fixed;
left: 0;
right: 0;
bottom: 0;
padding: 0.625rem 0.75rem 1.25rem;
background: rgba(255, 255, 255, 0.96);
box-shadow: 0 -0.1875rem 0.625rem rgba(0, 0, 0, 0.08);
}
.primary {
width: 100%;
height: 2.75rem;
border-radius: 31.21875rem;
background: #4c8dff;
color: #fff;
font-size: 1rem;
font-weight: 600;
}

View File

@@ -1,37 +1,158 @@
/**
.report { padding: 0.625rem; * 这里是uni-app内置的常用样式变量
*
* uni-app 官方扩展插件及插件市场https://ext.dcloud.net.cn上很多三方插件均使用了这些样式变量
* 如果你是插件开发者建议你使用scss预处理并在插件代码中直接使用这些变量无需 import 这个文件方便用户通过搭积木的方式开发整体风格一致的App
*
*/
/**
* 如果你是App开发者插件使用者你可以通过修改这些变量来定制自己的插件主题实现自定义主题功能
*
* 如果你的项目同样使用了scss预处理你也可以直接在你的 scss 代码中使用如下变量,同时无需 import 这个文件
*/
/* 颜色变量 */
/* 行为相关颜色 */
/* 藏青系主色(高亮) */
/* 文字基本颜色 */
/* 背景颜色 */
/* 边框颜色 */
/* 尺寸变量 */
/* 文字尺寸 */
/* 图片尺寸 */
/* Border Radius */
/* 水平间距 */
/* 垂直间距 */
/* 透明度 */
/* 文章场景相关 */
/* 表单控件尺寸(统一配置,避免页面内硬编码) */
.report {
padding: 0.75rem 0.625rem 1.125rem;
display: flex;
flex-direction: column;
gap: 0.5625rem;
} }
.modes { display: flex; gap: 0.375rem; margin-bottom: 0.4375rem; .header {
font-size: 1.0625rem;
font-weight: 700;
color: #1f2a44;
padding-left: 0.25rem;
} }
.mode-tab { flex: 1; text-align: center; padding: 0.5rem 0; border-radius: 31.21875rem; background: #f4f4f4; color: #666; border: 0.03125rem solid #e9e9e9; .toolbar {
display: flex;
align-items: center;
justify-content: center;
gap: 0.375rem;
background: #f7f9fc;
border-radius: 0.5rem;
padding: 0.5625rem;
} }
.mode-tab.active { background: #1aad19; color: #fff; border-color: #1aad19; font-weight: 700; .date {
min-width: 6.25rem;
padding: 0.375rem 0.5625rem;
border-radius: 0.375rem;
background: #fff;
border: 0.03125rem solid rgba(91, 107, 139, 0.16);
text-align: center;
color: #32445b;
} }
.toolbar { display: flex; align-items: center; gap: 0.25rem; background: #fff; padding: 0.4375rem 0.5rem; border-radius: 0.375rem; .tabs {
display: flex;
gap: 0.375rem;
justify-content: center;
} }
.date { padding: 0.3125rem 0.5rem; border: 0.03125rem solid #eee; border-radius: 0.25rem; .tab {
padding: 0.3125rem 0.625rem;
border-radius: 31.21875rem;
background: #f0f4ff;
color: #5b6b8b;
transition: all 0.2s ease;
} }
.tabs { display: flex; gap: 0.5rem; margin-top: 0.4375rem; .tab.active {
background: rgba(76, 141, 255, 0.18);
color: #3467d6;
box-shadow: inset 0 0 0 0.0625rem rgba(76, 141, 255, 0.45);
} }
.tab { padding: 0.375rem 0.5625rem; border-radius: 31.21875rem; background: #f4f4f4; color: #666; .summary {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(7.5rem, 1fr));
gap: 0.375rem;
} }
.tab.active { background: #1aad19; color: #fff; .summary-item {
background: #f7f9fc;
border-radius: 0.5rem;
padding: 0.625rem;
display: flex;
flex-direction: column;
gap: 0.3125rem;
} }
.summary { display: grid; grid-template-columns: repeat(4, 1fr); gap: 0.25rem; margin-top: 0.4375rem; .summary-item .label {
font-size: 0.75rem;
color: #6e7a96;
} }
.summary .item { background: #fff; border-radius: 0.375rem; padding: 0.5rem; .summary-item .value {
font-size: 1rem;
font-weight: 700;
color: #1f2a44;
} }
.summary .label { font-size: 0.6875rem; color: #888; .card {
background: #fff;
border-radius: 0.5625rem;
padding: 0.625rem;
box-shadow: 0 0.25rem 0.625rem rgba(31, 42, 68, 0.08);
display: flex;
flex-direction: column;
gap: 0.4375rem;
} }
.summary .value { display: block; margin-top: 0.25rem; font-weight: 700; color: #333; .row-head {
display: flex;
justify-content: space-between;
align-items: flex-start;
} }
.card { margin-top: 0.5rem; background: #fff; border-radius: 0.375rem; padding: 0.5rem; .row-title {
display: flex;
flex-direction: column;
gap: 0.1875rem;
} }
.row-head { display: flex; align-items: center; gap: 0.375rem; .title {
font-size: 0.9375rem;
font-weight: 700;
color: #1f2a44;
} }
.thumb { width: 2.25rem; height: 2.25rem; border-radius: 0.25rem; background: #f2f2f2; .subtitle {
font-size: 0.75rem;
color: #6e7a96;
} }
.title { font-size: 0.875rem; font-weight: 700; .row-body {
display: flex;
flex-wrap: wrap;
gap: 0.375rem 0.75rem;
} }
.row-body { margin-top: 0.3125rem; color: #666; .metric {
display: flex;
gap: 0.25rem;
align-items: center;
background: #f4f6fb;
border-radius: 0.375rem;
padding: 0.3125rem 0.5rem;
} }
.metric-label {
font-size: 0.75rem;
color: #6e7a96;
}
.metric-value {
font-size: 0.875rem;
color: #1f2a44;
font-weight: 600;
}
.empty {
text-align: center;
padding: 2.5rem 0;
color: #9aa4be;
font-size: 0.8125rem;
}
.loading {
text-align: center;
padding: 1.25rem 0;
color: #5b6b8b;
font-size: 0.75rem;
}

View File

@@ -1,15 +1,66 @@
/**
.page { padding-bottom: 4.375rem; * 这里是uni-app内置的常用样式变量
*
* uni-app 官方扩展插件及插件市场https://ext.dcloud.net.cn上很多三方插件均使用了这些样式变量
* 如果你是插件开发者建议你使用scss预处理并在插件代码中直接使用这些变量无需 import 这个文件方便用户通过搭积木的方式开发整体风格一致的App
*
*/
/**
* 如果你是App开发者插件使用者你可以通过修改这些变量来定制自己的插件主题实现自定义主题功能
*
* 如果你的项目同样使用了scss预处理你也可以直接在你的 scss 代码中使用如下变量,同时无需 import 这个文件
*/
/* 颜色变量 */
/* 行为相关颜色 */
/* 藏青系主色(高亮) */
/* 文字基本颜色 */
/* 背景颜色 */
/* 边框颜色 */
/* 尺寸变量 */
/* 文字尺寸 */
/* 图片尺寸 */
/* Border Radius */
/* 水平间距 */
/* 垂直间距 */
/* 透明度 */
/* 文章场景相关 */
/* 表单控件尺寸(统一配置,避免页面内硬编码) */
.page {
padding-bottom: 4.375rem;
} }
.field { display:flex; justify-content: space-between; padding: 0.6875rem 0.75rem; background:#fff; border-bottom:0.03125rem solid #eee; .field {
display: flex;
justify-content: space-between;
padding: 0.6875rem 0.75rem;
background: #ffffff;
border-bottom: 0.03125rem solid #e5e7eb;
} }
.label { color:#666; .label {
color: #444;
} }
.value { color:#333; text-align: right; flex: 1; .value {
color: #111;
text-align: right;
flex: 1;
} }
.textarea { padding: 0.5rem 0.75rem; background:#fff; margin-top: 0.375rem; .textarea {
padding: 0.5rem 0.75rem;
background: #ffffff;
margin-top: 0.375rem;
} }
.bottom { position: fixed; left:0; right:0; bottom:0; background:#fff; padding: 0.5rem 0.75rem calc(env(safe-area-inset-bottom) + 0.5rem); box-shadow: 0 -0.125rem 0.375rem rgba(0,0,0,0.06); .bottom {
} position: fixed;
.primary { width: 100%; background: linear-gradient(135deg, #A0E4FF 0%, #17A2C4 100%); color:#fff; border-radius: 31.21875rem; padding: 0.625rem 0; left: 0;
right: 0;
bottom: 0;
background: #ffffff;
padding: 0.5rem 0.75rem calc(env(safe-area-inset-bottom) + 0.5rem);
box-shadow: 0 -0.125rem 0.375rem rgba(0, 0, 0, 0.1);
} }
.primary {
width: 100%;
background: #4C8DFF;
color: #fff;
border-radius: 31.21875rem;
padding: 0.625rem 0;
}

Some files were not shown because too many files have changed in this diff Show More