refactor: 重构项目结构,迁移后端至 mengyastore-backend-go,新增 Java 后端、前端功能更新及部署文档
This commit is contained in:
36
mengyastore-frontend/package-lock.json
generated
36
mengyastore-frontend/package-lock.json
generated
@@ -2106,9 +2106,6 @@
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2126,9 +2123,6 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2146,9 +2140,6 @@
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2166,9 +2157,6 @@
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2186,9 +2174,6 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2206,9 +2191,6 @@
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2226,9 +2208,6 @@
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2252,9 +2231,6 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2278,9 +2254,6 @@
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2304,9 +2277,6 @@
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2330,9 +2300,6 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2356,9 +2323,6 @@
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
||||
@@ -120,7 +120,7 @@
|
||||
<script setup>
|
||||
import { computed, nextTick, onMounted, reactive, ref, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { fetchAdminToken, fetchStats, recordSiteVisit } from './modules/shared/api'
|
||||
import { verifyAdminToken, fetchStats, recordSiteVisit } from './modules/shared/api'
|
||||
import { authState, isLoggedIn, clearAuth, getLoginUrl } from './modules/shared/auth'
|
||||
import { wishlistCount, loadWishlist } from './modules/shared/useWishlist'
|
||||
import ChatWidget from './modules/chat/ChatWidget.vue'
|
||||
@@ -171,10 +171,10 @@ const submitAdminToken = async () => {
|
||||
if (!input) return
|
||||
tokenError.value = ''
|
||||
try {
|
||||
const correctToken = await fetchAdminToken()
|
||||
if (input === correctToken) {
|
||||
const valid = await verifyAdminToken(input)
|
||||
if (valid) {
|
||||
showAdminModal.value = false
|
||||
router.push(`/admin?token=${encodeURIComponent(correctToken)}`)
|
||||
router.push(`/admin?token=${encodeURIComponent(input)}`)
|
||||
} else {
|
||||
tokenError.value = '令牌错误,请重试'
|
||||
}
|
||||
|
||||
@@ -42,7 +42,6 @@
|
||||
v-model:token="token"
|
||||
:message="!token || message ? message : ''"
|
||||
:inline-message="token && message ? message : ''"
|
||||
@auto-get="autoGetToken"
|
||||
/>
|
||||
|
||||
<!-- Section: Products -->
|
||||
@@ -96,7 +95,6 @@ import { computed, onMounted, ref, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import {
|
||||
fetchAdminProducts,
|
||||
fetchAdminToken,
|
||||
createProduct,
|
||||
updateProduct,
|
||||
toggleProduct,
|
||||
@@ -183,12 +181,6 @@ const refresh = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const autoGetToken = async () => {
|
||||
const fetched = await fetchAdminToken()
|
||||
token.value = fetched
|
||||
syncQuery()
|
||||
await refresh()
|
||||
}
|
||||
|
||||
const loadMaintenance = async () => {
|
||||
try {
|
||||
|
||||
@@ -135,7 +135,7 @@ const load = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
conversations.value = await fetchAdminAllConversations(props.adminToken)
|
||||
// Refresh current conversation messages if one is selected
|
||||
// 若当前已选中某个会话,则刷新其消息列表
|
||||
if (selectedAccount.value) {
|
||||
currentMessages.value = conversations.value[selectedAccount.value] || []
|
||||
await scrollThreadBottom()
|
||||
|
||||
@@ -98,7 +98,7 @@ const pagedOrders = computed(() => {
|
||||
return props.orders.slice(start, start + PAGE_SIZE)
|
||||
})
|
||||
|
||||
// Reset to page 1 when orders list changes
|
||||
// 订单列表变化时重置到第一页
|
||||
watch(() => props.orders.length, () => { currentPage.value = 1 })
|
||||
|
||||
const remove = (id) => {
|
||||
|
||||
@@ -5,26 +5,35 @@
|
||||
<span class="smtp-desc tag">下单/发货时自动给用户发送通知邮件(支持 QQ / 163 / Gmail / 自定义域名邮箱)</span>
|
||||
<span v-if="message" class="msg-tag" :class="{ error: message.includes('失败') }">{{ message }}</span>
|
||||
</div>
|
||||
<div class="smtp-fields">
|
||||
<div class="smtp-enable-row">
|
||||
<label class="smtp-toggle">
|
||||
<input type="checkbox" v-model="form.enabled" />
|
||||
<span>启用邮件通知</span>
|
||||
</label>
|
||||
<span class="smtp-status-tag" :class="form.enabled ? 'tag-on' : 'tag-off'">
|
||||
{{ form.enabled ? '已启用' : '已关闭' }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="smtp-fields" :class="{ 'smtp-fields-disabled': !form.enabled }">
|
||||
<label class="smtp-field">
|
||||
<span>发件邮箱</span>
|
||||
<input v-model="form.email" type="email" placeholder="noreply@yourdomain.com" />
|
||||
<input v-model="form.email" type="email" placeholder="noreply@yourdomain.com" :disabled="!form.enabled" />
|
||||
</label>
|
||||
<label class="smtp-field">
|
||||
<span>SMTP 密码 / 授权码</span>
|
||||
<input v-model="form.password" type="password" placeholder="QQ/163 填授权码;其他填密码" autocomplete="new-password" />
|
||||
<input v-model="form.password" type="password" placeholder="QQ/163 填授权码;其他填密码" autocomplete="new-password" :disabled="!form.enabled" />
|
||||
</label>
|
||||
<label class="smtp-field">
|
||||
<span>发件人名称</span>
|
||||
<input v-model="form.fromName" type="text" placeholder="萌芽小店" />
|
||||
<input v-model="form.fromName" type="text" placeholder="萌芽小店" :disabled="!form.enabled" />
|
||||
</label>
|
||||
<label class="smtp-field">
|
||||
<span>SMTP 主机</span>
|
||||
<input v-model="form.host" type="text" placeholder="smtp.qq.com" />
|
||||
<input v-model="form.host" type="text" placeholder="smtp.qq.com" :disabled="!form.enabled" />
|
||||
</label>
|
||||
<label class="smtp-field smtp-field-port">
|
||||
<span>端口</span>
|
||||
<input v-model="form.port" type="text" placeholder="465" />
|
||||
<input v-model="form.port" type="text" placeholder="465" :disabled="!form.enabled" />
|
||||
</label>
|
||||
<button class="primary smtp-save-btn" type="button" :disabled="saving" @click="save">
|
||||
{{ saving ? '保存中...' : '保存配置' }}
|
||||
@@ -46,6 +55,7 @@ const emit = defineEmits(['save'])
|
||||
const saving = ref(false)
|
||||
|
||||
const form = reactive({
|
||||
enabled: true,
|
||||
email: '',
|
||||
password: '',
|
||||
fromName: '',
|
||||
@@ -55,6 +65,7 @@ const form = reactive({
|
||||
|
||||
watch(() => props.config, (cfg) => {
|
||||
if (!cfg) return
|
||||
form.enabled = cfg.enabled !== false
|
||||
form.email = cfg.email || ''
|
||||
form.password = cfg.password || ''
|
||||
form.fromName = cfg.fromName || ''
|
||||
@@ -101,6 +112,47 @@ const save = async () => {
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.smtp-enable-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.smtp-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
color: var(--text);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.smtp-toggle input[type="checkbox"] {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
accent-color: var(--accent);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.smtp-status-tag {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
padding: 2px 8px;
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.tag-on {
|
||||
background: rgba(74, 222, 128, 0.15);
|
||||
color: #2d8a4e;
|
||||
}
|
||||
|
||||
.tag-off {
|
||||
background: rgba(0,0,0,0.06);
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.smtp-fields {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
@@ -108,6 +160,11 @@ const save = async () => {
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.smtp-fields-disabled {
|
||||
opacity: 0.45;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.smtp-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -3,8 +3,7 @@
|
||||
<div class="form-field token-field">
|
||||
<label>管理 Token</label>
|
||||
<div class="token-input-wrap">
|
||||
<input :value="token" @input="$emit('update:token', $event.target.value)" placeholder="粘贴 token 后自动加载…" />
|
||||
<button class="ghost small" type="button" @click="$emit('auto-get')">自动获取</button>
|
||||
<input :value="token" @input="$emit('update:token', $event.target.value)" placeholder="粘贴管理员令牌后自动加载…" />
|
||||
</div>
|
||||
</div>
|
||||
<p
|
||||
@@ -28,7 +27,7 @@ defineProps({
|
||||
inlineMessage: { type: String, default: '' }
|
||||
})
|
||||
|
||||
defineEmits(['update:token', 'auto-get'])
|
||||
defineEmits(['update:token'])
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -52,7 +52,7 @@ onMounted(async () => {
|
||||
}
|
||||
|
||||
try {
|
||||
// Verify token and get up-to-date user info from SproutGate
|
||||
// 验证 token 并从 SproutGate 获取最新用户信息
|
||||
const verifyData = await verifySproutGateToken(token)
|
||||
if (!verifyData.valid) {
|
||||
status.value = 'error'
|
||||
@@ -71,7 +71,7 @@ onMounted(async () => {
|
||||
status.value = 'success'
|
||||
setTimeout(() => router.push('/'), 1000)
|
||||
} catch {
|
||||
// If verify fails (network issue), fall back to fragment data
|
||||
// 验证失败(如网络异常)时,回退使用 URL fragment 中的数据
|
||||
setAuth({ token, account, username, avatarUrl: fragmentAvatar })
|
||||
displayName.value = username || account
|
||||
status.value = 'success'
|
||||
|
||||
@@ -121,7 +121,7 @@ const loadMessages = async () => {
|
||||
messages.value = await fetchMyChatMessages(props.userToken)
|
||||
await scrollBottom()
|
||||
} catch {
|
||||
// silently ignore polling errors
|
||||
// 静默忽略轮询错误,避免频繁弹出错误提示
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,18 @@
|
||||
import axios from 'axios'
|
||||
|
||||
const apiBaseURL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8080'
|
||||
const apiBaseURL =
|
||||
import.meta.env.VITE_API_BASE_URL ||
|
||||
import.meta.env.VITE_API_BASE ||
|
||||
(typeof window !== 'undefined' ? window.location.origin : 'http://localhost:8080')
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: apiBaseURL
|
||||
})
|
||||
|
||||
// 管理员请求头构建辅助函数,使用 X-Admin-Token 请求头。
|
||||
// 后端保留 ?token= 查询参数作为旧版兼容,新请求统一使用请求头以兼容 Spring Security。
|
||||
const adminHeaders = (token) => ({ 'X-Admin-Token': token })
|
||||
|
||||
const authApi = axios.create({
|
||||
baseURL: 'https://auth.api.shumengya.top'
|
||||
})
|
||||
@@ -60,40 +67,40 @@ export const fetchMyOrders = async (authToken) => {
|
||||
return data.data || []
|
||||
}
|
||||
|
||||
export const fetchAdminToken = async () => {
|
||||
const { data } = await api.get('/api/admin/token')
|
||||
return data.token || ''
|
||||
export const verifyAdminToken = async (token) => {
|
||||
const { data } = await api.post('/api/admin/verify', { token })
|
||||
return data.valid === true
|
||||
}
|
||||
|
||||
export const fetchAdminProducts = async (token) => {
|
||||
const { data } = await api.get('/api/admin/products', { params: { token } })
|
||||
const { data } = await api.get('/api/admin/products', { headers: adminHeaders(token) })
|
||||
return data.data || []
|
||||
}
|
||||
|
||||
export const createProduct = async (token, payload) => {
|
||||
const { data } = await api.post('/api/admin/products', payload, {
|
||||
params: { token }
|
||||
headers: adminHeaders(token)
|
||||
})
|
||||
return data.data
|
||||
}
|
||||
|
||||
export const updateProduct = async (token, id, payload) => {
|
||||
const { data } = await api.put(`/api/admin/products/${id}`, payload, {
|
||||
params: { token }
|
||||
headers: adminHeaders(token)
|
||||
})
|
||||
return data.data
|
||||
}
|
||||
|
||||
export const toggleProduct = async (token, id, active) => {
|
||||
const { data } = await api.patch(`/api/admin/products/${id}/status`, { active }, {
|
||||
params: { token }
|
||||
headers: adminHeaders(token)
|
||||
})
|
||||
return data.data
|
||||
}
|
||||
|
||||
export const deleteProduct = async (token, id) => {
|
||||
const { data } = await api.delete(`/api/admin/products/${id}`, {
|
||||
params: { token }
|
||||
headers: adminHeaders(token)
|
||||
})
|
||||
return data
|
||||
}
|
||||
@@ -120,12 +127,12 @@ export const removeFromWishlist = async (token, productId) => {
|
||||
}
|
||||
|
||||
export const fetchAdminOrders = async (token) => {
|
||||
const { data } = await api.get('/api/admin/orders', { params: { token } })
|
||||
const { data } = await api.get('/api/admin/orders', { headers: adminHeaders(token) })
|
||||
return data.data || []
|
||||
}
|
||||
|
||||
export const deleteAdminOrder = async (token, orderId) => {
|
||||
await api.delete(`/api/admin/orders/${orderId}`, { params: { token } })
|
||||
await api.delete(`/api/admin/orders/${orderId}`, { headers: adminHeaders(token) })
|
||||
}
|
||||
|
||||
export const fetchSiteMaintenance = async () => {
|
||||
@@ -135,19 +142,19 @@ export const fetchSiteMaintenance = async () => {
|
||||
|
||||
export const setSiteMaintenance = async (token, maintenance, reason) => {
|
||||
const { data } = await api.post('/api/admin/site/maintenance', { maintenance, reason }, {
|
||||
params: { token }
|
||||
headers: adminHeaders(token)
|
||||
})
|
||||
return data.data || {}
|
||||
}
|
||||
|
||||
// ---- SMTP Config ----
|
||||
export const fetchSMTPConfig = async (token) => {
|
||||
const { data } = await api.get('/api/admin/site/smtp', { params: { token } })
|
||||
const { data } = await api.get('/api/admin/site/smtp', { headers: adminHeaders(token) })
|
||||
return data.data || {}
|
||||
}
|
||||
|
||||
export const setSMTPConfig = async (token, cfg) => {
|
||||
const { data } = await api.post('/api/admin/site/smtp', cfg, { params: { token } })
|
||||
const { data } = await api.post('/api/admin/site/smtp', cfg, { headers: adminHeaders(token) })
|
||||
return data.data
|
||||
}
|
||||
|
||||
@@ -168,13 +175,13 @@ export const sendChatMessage = async (userToken, content) => {
|
||||
|
||||
// ---- Chat (admin) ----
|
||||
export const fetchAdminAllConversations = async (adminToken) => {
|
||||
const { data } = await api.get('/api/admin/chat', { params: { token: adminToken } })
|
||||
const { data } = await api.get('/api/admin/chat', { headers: adminHeaders(adminToken) })
|
||||
return data.data?.conversations || {}
|
||||
}
|
||||
|
||||
export const fetchAdminConversation = async (adminToken, account) => {
|
||||
const { data } = await api.get(`/api/admin/chat/${encodeURIComponent(account)}`, {
|
||||
params: { token: adminToken }
|
||||
headers: adminHeaders(adminToken)
|
||||
})
|
||||
return data.data?.messages || []
|
||||
}
|
||||
@@ -183,13 +190,13 @@ export const adminSendChatReply = async (adminToken, account, content) => {
|
||||
const { data } = await api.post(
|
||||
`/api/admin/chat/${encodeURIComponent(account)}`,
|
||||
{ content },
|
||||
{ params: { token: adminToken } }
|
||||
{ headers: adminHeaders(adminToken) }
|
||||
)
|
||||
return data.data?.message || null
|
||||
}
|
||||
|
||||
export const adminClearConversation = async (adminToken, account) => {
|
||||
await api.delete(`/api/admin/chat/${encodeURIComponent(account)}`, {
|
||||
params: { token: adminToken }
|
||||
headers: adminHeaders(adminToken)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ const addToWishlist = async (productId) => {
|
||||
try {
|
||||
wishlistIds.value = await apiAddToWishlist(authState.token, productId)
|
||||
} catch {
|
||||
// ignore
|
||||
// 忽略错误
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ const removeFromWishlist = async (productId) => {
|
||||
try {
|
||||
wishlistIds.value = await apiRemoveFromWishlist(authState.token, productId)
|
||||
} catch {
|
||||
// ignore
|
||||
// 忽略错误
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user