diff --git a/backend/main.py b/backend/main.py
index 0c79e1b..db3ddcd 100644
--- a/backend/main.py
+++ b/backend/main.py
@@ -501,7 +501,7 @@ def get_my_business_application(user=Depends(get_current_user)):
def list_business_applications(user=Depends(require_role("admin"))):
conn = get_db()
rows = conn.execute(
- "SELECT a.id, a.user_id, a.business_name, a.document, a.status, a.created_at, "
+ "SELECT a.id, a.user_id, a.business_name, a.document, a.status, a.reject_reason, a.created_at, "
"u.display_name, u.username FROM business_applications a "
"LEFT JOIN users u ON a.user_id = u.id ORDER BY a.id DESC"
).fetchall()
diff --git a/frontend/src/App.vue b/frontend/src/App.vue
index 3de03fe..c21ba42 100644
--- a/frontend/src/App.vue
+++ b/frontend/src/App.vue
@@ -82,9 +82,6 @@ const allTabs = [
{ key: 'inventory', icon: '📦', label: '个人库存', require: 'login' },
{ key: 'oils', icon: '💧', label: '精油价目' },
{ key: 'projects', icon: '💼', label: '商业核算', require: 'login' },
- { key: 'audit', icon: '📜', label: '操作日志', hide: 'admin' },
- { key: 'bugs', icon: '🐛', label: 'Bug', hide: 'admin' },
- { key: 'users', icon: '👥', label: '用户管理', hide: 'admin' },
]
// 所有人都能看到大部分 tab,bug 和用户管理只有 admin 可见
diff --git a/frontend/src/components/UserMenu.vue b/frontend/src/components/UserMenu.vue
index 32e5d81..d72ff52 100644
--- a/frontend/src/components/UserMenu.vue
+++ b/frontend/src/components/UserMenu.vue
@@ -14,6 +14,11 @@
+
+
+
+
+
@@ -88,6 +93,11 @@ function goMyDiary() {
router.push('/mydiary')
}
+function goAdmin(section) {
+ emit('close')
+ router.push('/' + section)
+}
+
function toggleNotifications() {
showNotifPanel.value = !showNotifPanel.value
showBugForm.value = false
diff --git a/frontend/src/views/MyDiary.vue b/frontend/src/views/MyDiary.vue
index a9632eb..5dc58d1 100644
--- a/frontend/src/views/MyDiary.vue
+++ b/frontend/src/views/MyDiary.vue
@@ -241,56 +241,58 @@
-
-
💼 商业认证
+
+
🏢 商业用户认证
-
-
✅
-
已认证商业用户
+
+ ✅ 已认证商业用户
-
-
-
⏳
-
认证申请审核中
-
商户名:{{ bizApp.business_name }}
-
提交时间:{{ formatDate(bizApp.created_at) }}
-
-
+
+
⏳ 认证申请审核中
+
商户名:{{ bizApp.business_name }} · 提交时间:{{ formatDate(bizApp.created_at) }}
+
-
+
-
-
❌
-
认证申请未通过
-
原因:{{ bizApp.reject_reason }}
+
+
❌ 认证申请未通过
+
原因:{{ bizApp.reject_reason }}
你可以修改信息后重新申请。
-
-
-
-
-
-
-
-
-
-
-
- 申请商业认证后可使用商业核算功能,请填写以下信息。
-
@@ -341,6 +343,8 @@ const newPassword = ref('')
const confirmPassword = ref('')
const businessName = ref('')
const businessReason = ref('')
+const bizType = ref('')
+const bizPhone = ref('')
const bizApp = ref({ status: null })
onMounted(async () => {
@@ -669,24 +673,30 @@ async function changePassword() {
}
async function applyBusiness() {
- if (!businessName.value.trim()) {
- ui.showToast('请填写商户名称')
+ if (!businessName.value.trim() || !bizType.value) {
+ ui.showToast('请填写必填项')
return
}
+ const typeLabels = { individual: '个体经营户', company: '公司', studio: '工作室/美容院', distributor: '代理商' }
+ const info = [
+ `认证类型:${typeLabels[bizType.value] || bizType.value}`,
+ bizPhone.value ? `联系电话:${bizPhone.value}` : '',
+ businessReason.value ? `业务描述:${businessReason.value}` : '',
+ ].filter(Boolean).join('\n')
try {
const res = await api('/api/business-apply', {
method: 'POST',
body: JSON.stringify({
business_name: businessName.value.trim(),
- document: businessReason.value.trim(),
+ document: info,
}),
})
if (res.ok) {
businessName.value = ''
businessReason.value = ''
- bizApp.value = { status: 'pending', business_name: businessName.value }
+ bizType.value = ''
+ bizPhone.value = ''
ui.showToast('申请已提交,请等待管理员审核')
- // Reload status
const bizRes = await api('/api/my-business-application')
if (bizRes.ok) bizApp.value = await bizRes.json()
} else {
@@ -1147,19 +1157,27 @@ async function applyBusiness() {
text-align: center;
}
-.biz-status {
- padding: 16px;
- border-radius: 12px;
- text-align: center;
+.biz-card { border-radius: 16px; }
+.biz-status-bar {
+ padding: 12px 16px;
+ border-radius: 10px;
+ font-size: 14px;
+ font-weight: 500;
margin-bottom: 12px;
+ line-height: 1.6;
}
-.biz-status-icon { font-size: 32px; margin-bottom: 8px; }
-.biz-status-text { font-size: 15px; font-weight: 600; margin-bottom: 4px; }
-.biz-status-detail { font-size: 12px; color: #999; }
-.biz-approved { background: #e8f5e9; color: #2e7d5a; }
-.biz-pending { background: #fff8e1; color: #e65100; }
-.biz-rejected { background: #fce4ec; color: #c62828; }
-.biz-reject-reason { font-size: 13px; margin-top: 8px; padding: 8px 12px; background: rgba(0,0,0,0.05); border-radius: 8px; }
+.biz-status-bar.biz-approved { background: #e8f5e9; color: #2e7d32; border-left: 3px solid #4caf50; }
+.biz-status-bar.biz-pending { background: #fff3e0; color: #e65100; border-left: 3px solid #ff9800; }
+.biz-status-bar.biz-rejected { background: #ffebee; color: #c62828; border-left: 3px solid #f44336; }
+.biz-status-detail { font-size: 12px; margin-top: 4px; opacity: 0.8; }
+.biz-form { margin-top: 8px; }
+.biz-form .form-group { margin-bottom: 14px; }
+.biz-form .form-label { display: block; font-size: 13px; font-weight: 600; color: #3e3a44; margin-bottom: 6px; }
+.biz-form .form-select {
+ width: 100%; padding: 10px 14px; border: 1.5px solid #d4cfc7; border-radius: 10px;
+ font-size: 14px; font-family: inherit; background: #fff; outline: none; box-sizing: border-box;
+}
+.biz-form .form-select:focus { border-color: #7ec6a4; }
/* Buttons */
.btn-primary {
diff --git a/frontend/src/views/Projects.vue b/frontend/src/views/Projects.vue
index 1b896ff..a3b8b88 100644
--- a/frontend/src/views/Projects.vue
+++ b/frontend/src/views/Projects.vue
@@ -42,26 +42,53 @@
-
+
-
🧴 配方成分
-
-
-
-
{{ ing.oil ? oils.fmtPrice(oils.pricePerDrop(ing.oil) * (ing.drops || 0)) : '--' }}
-
+
+
+
+ 配方总成本
+ {{ oils.fmtPrice(materialCost) }}
+
+
+
+
-
@@ -492,12 +519,38 @@ function formatDate(d) {
color: #3e3a44;
}
-.ing-row {
- display: flex;
- gap: 6px;
- align-items: center;
- margin-bottom: 6px;
+.section-header-row {
+ display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px;
}
+.section-header-row h4 { margin: 0; }
+.section-actions { display: flex; gap: 6px; }
+
+.ingredients-table { width: 100%; border-collapse: collapse; margin-bottom: 12px; }
+.ingredients-table th {
+ text-align: center; padding: 10px 8px; font-size: 12px; font-weight: 600;
+ color: var(--text-light, #999); border-bottom: 2px solid #e5e4e7;
+}
+.ingredients-table td { padding: 10px 8px; border-bottom: 1px solid #f0f0f0; text-align: center; }
+.ingredients-table .form-select { width: 100%; padding: 6px 8px; border: 1.5px solid #d4cfc7; border-radius: 8px; font-size: 13px; font-family: inherit; background: #fff; }
+.drops-input { width: 65px; padding: 6px 8px; border: 1.5px solid #d4cfc7; border-radius: 8px; font-size: 13px; text-align: center; outline: none; font-family: inherit; }
+.drops-input:focus { border-color: #7ec6a4; }
+.cell-ppd { color: #999; font-size: 12px; }
+.cell-subtotal { color: #4a9d7e; font-weight: 600; }
+.remove-btn { border: none; background: none; color: #ccc; cursor: pointer; font-size: 18px; }
+.remove-btn:hover { color: #c0392b; }
+
+.total-row {
+ background: #e8f5e9; border-radius: 12px; padding: 14px 18px;
+ display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px;
+}
+.total-label { font-size: 14px; color: #3e3a44; font-weight: 500; }
+.total-price { font-size: 20px; font-weight: 700; color: #2e7d5a; }
+
+.pricing-inline { margin-top: 12px; }
+.price-field { display: flex; align-items: center; gap: 8px; }
+.price-field label { font-size: 13px; font-weight: 600; color: #3e3a44; white-space: nowrap; }
+.price-input { width: 100px; padding: 8px 10px; border: 1.5px solid #d4cfc7; border-radius: 8px; font-size: 14px; font-family: inherit; outline: none; }
+.price-input:focus { border-color: #7ec6a4; }
.form-select {
flex: 1;
@@ -520,14 +573,6 @@ function formatDate(d) {
text-align: center;
}
-.ing-cost {
- font-size: 13px;
- color: #4a9d7e;
- font-weight: 500;
- min-width: 60px;
- text-align: right;
-}
-
.price-row {
display: flex;
justify-content: space-between;
diff --git a/frontend/src/views/RecipeManager.vue b/frontend/src/views/RecipeManager.vue
index e678457..4dde55e 100644
--- a/frontend/src/views/RecipeManager.vue
+++ b/frontend/src/views/RecipeManager.vue
@@ -26,7 +26,6 @@
-
@@ -40,6 +39,7 @@
:class="selectedDiaryIds.size > 0 ? 'btn-select-active' : 'btn-outline'"
@click="toggleSelectAllDiary"
>全选
+
diff --git a/frontend/src/views/UserManagement.vue b/frontend/src/views/UserManagement.vue
index e299dbd..9759a80 100644
--- a/frontend/src/views/UserManagement.vue
+++ b/frontend/src/views/UserManagement.vue
@@ -22,17 +22,33 @@
-
+
💼 商业认证申请
-
-
-
{{ app.display_name || app.username }}
-
商户名:{{ app.business_name }}
+
+
+
+ {{ group.latest.display_name || group.latest.username }}
+ 商户名:{{ group.latest.business_name }}
+ {{ { pending: '待审核', approved: '已通过', rejected: '已拒绝' }[group.latest.status] }}
+
+
+
+
+
+
+
+
-
-
-
+
+
+ {{ { pending: '待审核', approved: '已通过', rejected: '已拒绝' }[app.status] }}
+ {{ app.business_name }}
+ 拒绝原因:{{ app.reject_reason }}
+ {{ formatDate(app.created_at) }}
+
@@ -111,6 +127,27 @@ const searchQuery = ref('')
const filterRole = ref('')
const translations = ref([])
const businessApps = ref([])
+import { reactive } from 'vue'
+
+const groupedBizApps = computed(() => {
+ const map = {}
+ for (const app of businessApps.value) {
+ const uid = app.user_id
+ if (!map[uid]) map[uid] = { user_id: uid, history: [], latest: null, expanded: false }
+ map[uid].history.push(app)
+ }
+ return Object.values(map).map(g => {
+ g.history.sort((a, b) => b.id - a.id)
+ g.latest = g.history[0]
+ return reactive(g)
+ }).filter(g => g.latest)
+})
+
+function formatDate(d) {
+ if (!d) return ''
+ return new Date(d + 'Z').toLocaleString('zh-CN', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' })
+}
+
const roles = [
{ value: 'admin', label: '管理员' },
{ value: 'senior_editor', label: '高级编辑' },
@@ -138,10 +175,6 @@ function roleLabel(role) {
return map[role] || role
}
-function formatDate(d) {
- if (!d) return '--'
- return new Date(d).toLocaleDateString('zh-CN')
-}
async function loadUsers() {
try {
@@ -352,8 +385,26 @@ onMounted(() => {
.review-actions {
display: flex;
gap: 6px;
+ flex-shrink: 0;
}
+.biz-app-group { margin-bottom: 6px; }
+.biz-status-tag {
+ font-size: 11px; padding: 2px 8px; border-radius: 8px; font-weight: 500; white-space: nowrap;
+}
+.biz-status-tag.small { font-size: 10px; padding: 1px 6px; }
+.biz-pending { background: #fff3e0; color: #e65100; }
+.biz-approved { background: #e8f5e9; color: #2e7d32; }
+.biz-rejected { background: #fce4ec; color: #c62828; }
+.biz-history {
+ margin: 4px 0 8px 16px; padding: 8px 12px; background: #fafaf8; border-radius: 8px; border-left: 3px solid #e5e4e7;
+}
+.biz-history-item {
+ display: flex; align-items: center; gap: 8px; font-size: 12px; padding: 4px 0; flex-wrap: wrap;
+}
+.biz-reject-reason { color: #c62828; font-size: 11px; }
+.biz-time { color: #bbb; font-size: 11px; margin-left: auto; }
+
.btn-approve {
background: #4a9d7e;
color: #fff;