新增对接接口,文章
|
|
@ -12,8 +12,8 @@
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, provide, onMounted } from 'vue'
|
import { ref, provide, onMounted } from 'vue'
|
||||||
import AppHeader from '@/components/AppHeader.vue'
|
import AppHeader from '@/components/common/AppHeader.vue'
|
||||||
import AppFooter from '@/components/AppFooter.vue'
|
import AppFooter from '@/components/common/AppFooter.vue'
|
||||||
import PaymentDialog from '@/components/PaymentDialog.vue'
|
import PaymentDialog from '@/components/PaymentDialog.vue'
|
||||||
import { useUserStore } from '@/stores/user'
|
import { useUserStore } from '@/stores/user'
|
||||||
import api from '@/api'
|
import api from '@/api'
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ export default {
|
||||||
/**
|
/**
|
||||||
* 3. 获取微信扫码登录二维码
|
* 3. 获取微信扫码登录二维码
|
||||||
*/
|
*/
|
||||||
async getLoginCode(data = {}) {
|
async getLoginCode(data) {
|
||||||
return await postJson(appConfig.baseURL, "/weixin/qrcode", data);
|
return await postJson(appConfig.baseURL, "/weixin/qrcode", data);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -39,9 +39,9 @@ export default {
|
||||||
* 4. 轮询微信扫码状态
|
* 4. 轮询微信扫码状态
|
||||||
* @param {Object} data - { key: 'xxx' }
|
* @param {Object} data - { key: 'xxx' }
|
||||||
*/
|
*/
|
||||||
async askLoginCode(data) {
|
// async askLoginCode(data) {
|
||||||
return await get(appConfig.baseURL, "/weixin/qrcode", data);
|
// return await get(appConfig.baseURL, "/weixin/qrcode", data);
|
||||||
},
|
// },
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 5. 获取当前登录用户信息
|
* 5. 获取当前登录用户信息
|
||||||
|
|
@ -68,5 +68,22 @@ export default {
|
||||||
return await postJson(appConfig.baseURL, "/order", data);
|
return await postJson(appConfig.baseURL, "/order", data);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询订单
|
||||||
|
* @param {*} data
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
async getOrder(data = {}) {
|
||||||
|
return await get(appConfig.baseURL, "/order", data);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取app二维码
|
||||||
|
* @param {*} data
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
async getAppCode(data) {
|
||||||
|
return await get(appConfig.baseURL, "/app/code", data);
|
||||||
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -73,7 +73,7 @@ import { Close } from '@element-plus/icons-vue'
|
||||||
import api from '@/api'
|
import api from '@/api'
|
||||||
import authService from '@/api/authService'
|
import authService from '@/api/authService'
|
||||||
import { useUserStore } from '@/stores/user'
|
import { useUserStore } from '@/stores/user'
|
||||||
import BaseDialog from './BaseDialog.vue'
|
import BaseDialog from '@/components/common/BaseDialog.vue'
|
||||||
|
|
||||||
const emit = defineEmits(['success'])
|
const emit = defineEmits(['success'])
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
|
|
@ -233,12 +233,13 @@ async function handleLogin() {
|
||||||
const res = await authService.login(loginPayload)
|
const res = await authService.login(loginPayload)
|
||||||
if (res && res.data && res.data.token) {
|
if (res && res.data && res.data.token) {
|
||||||
localStorage.setItem('token', res.data.token)
|
localStorage.setItem('token', res.data.token)
|
||||||
userStore.setUser(res.data, form.phone)
|
|
||||||
console.log("登录成功", res);
|
console.log("登录成功", res);
|
||||||
ElMessage.success('登录成功')
|
ElMessage.success('登录成功')
|
||||||
visible.value = false
|
visible.value = false
|
||||||
emit('success')
|
emit('success')
|
||||||
// window.location.reload()
|
const userInfo = (await authService.getUserInfo()).data
|
||||||
|
userStore.setUser(userInfo, form.phone)
|
||||||
|
window.location.reload()
|
||||||
} else {
|
} else {
|
||||||
showError.value = true
|
showError.value = true
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,14 @@
|
||||||
<div class="recharge-dialog">
|
<div class="recharge-dialog">
|
||||||
<!-- 头部用户信息 -->
|
<!-- 头部用户信息 -->
|
||||||
<div class="user-header">
|
<div class="user-header">
|
||||||
<el-avatar :size="45" :src="userInfo.avatar_url || undefined">
|
<el-avatar :size="45"
|
||||||
{{ userInfo.username?.charAt(0)?.toUpperCase() }}
|
:src="userInfo.avater || userInfo.avatar || userInfo.avatar_url || userStore.avatarUrl || undefined">
|
||||||
|
{{ (userInfo.name || userInfo.username || userStore.username || 'U').charAt(0).toUpperCase() }}
|
||||||
</el-avatar>
|
</el-avatar>
|
||||||
<div class="user-meta">
|
<div class="user-meta">
|
||||||
<span class="username">{{ userInfo.username || '用户' }}</span>
|
<span class="username">{{ userInfo.name || userInfo.username || userStore.username || '用户' }}</span>
|
||||||
<span class="vip-status" :class="{ expired: !isVip }">
|
<span class="vip-status" :class="{ expired: !isVip }">
|
||||||
{{ isVip ? `会员 ${formatExpire(userInfo.membership_expire_at)}到期` : '未开通会员' }}
|
{{ isVip ? `会员 ${userInfo.membership_expire_at}到期` : '未开通会员' }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -17,8 +18,10 @@
|
||||||
<div class="dialog-body">
|
<div class="dialog-body">
|
||||||
<!-- 左侧面板 -->
|
<!-- 左侧面板 -->
|
||||||
<div class="left-panel">
|
<div class="left-panel">
|
||||||
<div v-if="!loading && productList.length" class="plans-scroll" ref="scrollContainer" @mousedown="onMouseDown"
|
|
||||||
@mouseleave="onMouseLeave" @mouseup="onMouseUp" @mousemove="onMouseMove">
|
<div v-if="!loading && productList.length" class="plans-scroll" :class="{ 'is-disabled': paying }"
|
||||||
|
ref="scrollContainer" @mousedown="onMouseDown" @mouseleave="onMouseLeave" @mouseup="onMouseUp"
|
||||||
|
@mousemove="onMouseMove">
|
||||||
<div v-for="(item, idx) in productList" :key="item.goods_id" class="plan-card"
|
<div v-for="(item, idx) in productList" :key="item.goods_id" class="plan-card"
|
||||||
:class="{ active: selectedProductId === item.goods_id }" @click="handleProductChange(item.goods_id)">
|
:class="{ active: selectedProductId === item.goods_id }" @click="handleProductChange(item.goods_id)">
|
||||||
<div class="plan-name">{{ item.goods_name }}</div>
|
<div class="plan-name">{{ item.goods_name }}</div>
|
||||||
|
|
@ -32,6 +35,15 @@
|
||||||
</el-icon>
|
</el-icon>
|
||||||
<span>加载商品...</span>
|
<span>加载商品...</span>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- 手写切换加载蒙层 -->
|
||||||
|
<transition name="fade">
|
||||||
|
<div v-if="switchingProduct" class="custom-loading-mask">
|
||||||
|
<el-icon class="is-loading">
|
||||||
|
<Loading />
|
||||||
|
</el-icon>
|
||||||
|
<span>切换中...</span>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
|
||||||
<!-- 会员权益 -->
|
<!-- 会员权益 -->
|
||||||
<div class="privilege-section">
|
<div class="privilege-section">
|
||||||
|
|
@ -87,11 +99,17 @@
|
||||||
|
|
||||||
<div class="qr-box">
|
<div class="qr-box">
|
||||||
<img v-if="qrCode" :src="qrCode" alt="支付二维码" class="qr-img" />
|
<img v-if="qrCode" :src="qrCode" alt="支付二维码" class="qr-img" />
|
||||||
<div v-else class="qr-placeholder" @click="handlePay">
|
<div v-else-if="paying || loading" class="qr-placeholder loading">
|
||||||
<el-icon v-if="paying" class="is-loading">
|
<el-icon class="is-loading">
|
||||||
<Loading />
|
<Loading />
|
||||||
</el-icon>
|
</el-icon>
|
||||||
<span v-else>点击获取支付码</span>
|
<span>生成支付码...</span>
|
||||||
|
</div>
|
||||||
|
<div v-else class="qr-placeholder error" @click="handlePay">
|
||||||
|
<el-icon>
|
||||||
|
<RefreshRight />
|
||||||
|
</el-icon>
|
||||||
|
<span>获取失败,点击重试</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -119,10 +137,10 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onUnmounted } from 'vue'
|
import { ref, computed, watch, onUnmounted } from 'vue'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import BaseDialog from './BaseDialog.vue'
|
import BaseDialog from '@/components/common/BaseDialog.vue'
|
||||||
import { Loading, Download, ChatDotRound, Tickets, Postcard, Position, Van, ShoppingBag, Wallet } from '@element-plus/icons-vue'
|
import { Loading, RefreshRight, Download, ChatDotRound, Tickets, Postcard, Position, Van, ShoppingBag, Wallet } from '@element-plus/icons-vue'
|
||||||
import authService from '@/api/authService.js'
|
import authService from '@/api/authService.js'
|
||||||
import { useUserStore } from '@/stores/user'
|
import { useUserStore } from '@/stores/user'
|
||||||
|
|
||||||
|
|
@ -132,11 +150,21 @@ const userStore = useUserStore()
|
||||||
const visible = ref(false)
|
const visible = ref(false)
|
||||||
const selectedProductId = ref(null)
|
const selectedProductId = ref(null)
|
||||||
const paying = ref(false)
|
const paying = ref(false)
|
||||||
|
const switchingProduct = ref(false)
|
||||||
const qrCode = ref('')
|
const qrCode = ref('')
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const productList = ref([])
|
const productList = ref([])
|
||||||
const payMethod = ref('weixin')
|
const payMethod = ref('weixin')
|
||||||
let pollTimer = null
|
let pollTimer = null
|
||||||
|
let orderVersion = 0
|
||||||
|
let activeOrderNo = ''
|
||||||
|
|
||||||
|
function stopPolling() {
|
||||||
|
if (pollTimer) {
|
||||||
|
clearInterval(pollTimer)
|
||||||
|
pollTimer = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 拖拽滚动相关状态
|
// 拖拽滚动相关状态
|
||||||
const scrollContainer = ref(null)
|
const scrollContainer = ref(null)
|
||||||
|
|
@ -162,10 +190,14 @@ const currentOriginalPrice = computed(() => {
|
||||||
})
|
})
|
||||||
|
|
||||||
function open() {
|
function open() {
|
||||||
|
stopPolling()
|
||||||
|
orderVersion++
|
||||||
|
activeOrderNo = ''
|
||||||
visible.value = true
|
visible.value = true
|
||||||
selectedProductId.value = null
|
selectedProductId.value = null
|
||||||
qrCode.value = ''
|
qrCode.value = ''
|
||||||
paying.value = false
|
paying.value = false
|
||||||
|
switchingProduct.value = false
|
||||||
loadProducts()
|
loadProducts()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -177,16 +209,23 @@ async function loadProducts() {
|
||||||
productList.value = Array.isArray(res.data) ? res.data : []
|
productList.value = Array.isArray(res.data) ? res.data : []
|
||||||
|
|
||||||
if (productList.value.length > 0) {
|
if (productList.value.length > 0) {
|
||||||
// 默认选中第一个
|
// 默认选中第一个商品
|
||||||
selectedProductId.value = productList.value[0].goods_id
|
selectedProductId.value = productList.value[0].goods_id
|
||||||
|
} else {
|
||||||
|
ElMessage.warning('暂无可售商品')
|
||||||
}
|
}
|
||||||
if (!productList.value.length) ElMessage.warning('暂无可售商品')
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
ElMessage.error(e?.response?.data?.error || '加载商品失败')
|
ElMessage.error(e?.response?.data?.error || '加载商品失败')
|
||||||
productList.value = []
|
productList.value = []
|
||||||
} finally {
|
} finally {
|
||||||
|
// 优先关闭列表 Loading,让商品卡片先行渲染展示
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 列表渲染展示后,再触发支付二维码的异步获取
|
||||||
|
if (selectedProductId.value) {
|
||||||
|
handlePay()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -224,8 +263,16 @@ function getQrCodeUrl(resData) {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handlePay() {
|
async function handlePay() {
|
||||||
if (!selectedProductId.value) return
|
if (!selectedProductId.value || paying.value) return
|
||||||
|
|
||||||
|
stopPolling()
|
||||||
|
orderVersion++
|
||||||
|
const currentVersion = orderVersion
|
||||||
|
|
||||||
paying.value = true
|
paying.value = true
|
||||||
|
qrCode.value = ''
|
||||||
|
activeOrderNo = ''
|
||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
"goods_id": selectedProductId.value, // 从商品列表获取
|
"goods_id": selectedProductId.value, // 从商品列表获取
|
||||||
"pay_type": payMethod.value, // 支付方式,weixin/alipay/apple/bank/balance
|
"pay_type": payMethod.value, // 支付方式,weixin/alipay/apple/bank/balance
|
||||||
|
|
@ -238,6 +285,13 @@ async function handlePay() {
|
||||||
try {
|
try {
|
||||||
const res = await authService.createOrder(data)
|
const res = await authService.createOrder(data)
|
||||||
console.log('创建订单响应结果:', res)
|
console.log('创建订单响应结果:', res)
|
||||||
|
|
||||||
|
// 版本锁校验:若当前请求版本已被后续操作覆盖,则丢弃过时的响应
|
||||||
|
if (currentVersion !== orderVersion) {
|
||||||
|
console.warn('丢弃已失效过时的订单响应')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const orderData = res.data || res
|
const orderData = res.data || res
|
||||||
|
|
||||||
if (payMethod.value === 'alipay') {
|
if (payMethod.value === 'alipay') {
|
||||||
|
|
@ -247,32 +301,35 @@ async function handlePay() {
|
||||||
qrCode.value = getQrCodeUrl(orderData)
|
qrCode.value = getQrCodeUrl(orderData)
|
||||||
}
|
}
|
||||||
|
|
||||||
const orderNo = res.order_no || orderData.order_no || orderData.orderId
|
const orderNo = res.order_no || orderData.order_no || orderData.orderId || orderData.order_id
|
||||||
if (orderNo) {
|
if (orderNo) {
|
||||||
startPolling(orderNo)
|
activeOrderNo = orderNo
|
||||||
|
startPolling(orderNo, currentVersion)
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
ElMessage.error(e?.response?.data?.error || '创建订单失败')
|
if (currentVersion === orderVersion) {
|
||||||
|
qrCode.value = ''
|
||||||
|
ElMessage.error(e?.response?.data?.error || '创建订单失败,请点击重试')
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
paying.value = false
|
if (currentVersion === orderVersion) {
|
||||||
|
paying.value = false
|
||||||
|
switchingProduct.value = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handlePayMethodChange(method) {
|
function handlePayMethodChange(method) {
|
||||||
if (payMethod.value === method) return
|
if (paying.value || payMethod.value === method) return
|
||||||
payMethod.value = method
|
payMethod.value = method
|
||||||
if (qrCode.value) {
|
handlePay()
|
||||||
handlePay()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleProductChange(id) {
|
function handleProductChange(id) {
|
||||||
if (hasDragged) return
|
if (hasDragged || paying.value || selectedProductId.value === id) return
|
||||||
if (selectedProductId.value === id) return
|
|
||||||
selectedProductId.value = id
|
selectedProductId.value = id
|
||||||
if (qrCode.value) {
|
switchingProduct.value = true
|
||||||
handlePay()
|
handlePay()
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 拖拽滚动逻辑
|
// 拖拽滚动逻辑
|
||||||
|
|
@ -306,40 +363,48 @@ function onMouseMove(e) {
|
||||||
scrollContainer.value.scrollLeft = scrollLeft.value - walk
|
scrollContainer.value.scrollLeft = scrollLeft.value - walk
|
||||||
}
|
}
|
||||||
|
|
||||||
function startPolling(orderNo) {
|
function startPolling(orderNo, currentVersion) {
|
||||||
if (pollTimer) clearInterval(pollTimer)
|
stopPolling()
|
||||||
let count = 0
|
let count = 0
|
||||||
pollTimer = setInterval(async () => {
|
pollTimer = setInterval(async () => {
|
||||||
|
// 双重失效保护:若版本变动或关联的订单号不匹配,强行关闭轮询
|
||||||
|
if (currentVersion !== orderVersion || orderNo !== activeOrderNo) {
|
||||||
|
stopPolling()
|
||||||
|
return
|
||||||
|
}
|
||||||
count++
|
count++
|
||||||
try {
|
try {
|
||||||
const res = await api.payment.getOrder(orderNo)
|
const res = await authService.getOrder({ order_id: orderNo })
|
||||||
if (res.status === 'paid') {
|
console.log('轮询查询订单响应结果:', res, '订单号', orderNo)
|
||||||
clearInterval(pollTimer)
|
if (res.code == 0 && res?.data?.status ==
|
||||||
|
2) {
|
||||||
|
stopPolling()
|
||||||
ElMessage.success('支付成功,会员已开通')
|
ElMessage.success('支付成功,会员已开通')
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
visible.value = false
|
visible.value = false
|
||||||
emit('success')
|
emit('success')
|
||||||
}, 1500)
|
}, 1500)
|
||||||
}
|
}
|
||||||
} catch { }
|
} catch (e) {
|
||||||
|
console.error('轮询查询订单失败:', e)
|
||||||
|
}
|
||||||
if (count > 120) {
|
if (count > 120) {
|
||||||
clearInterval(pollTimer)
|
stopPolling()
|
||||||
ElMessage.warning('轮询超时,请刷新页面查看')
|
ElMessage.warning('轮询超时,请刷新页面查看')
|
||||||
}
|
}
|
||||||
}, 3000)
|
}, 3000)
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatDate(d) {
|
watch(visible, (val) => {
|
||||||
if (!d) return '-'
|
if (!val) {
|
||||||
return new Date(d).toLocaleString('zh-CN')
|
stopPolling()
|
||||||
}
|
orderVersion++
|
||||||
function formatExpire(d) {
|
activeOrderNo = ''
|
||||||
if (!d) return ''
|
}
|
||||||
return new Date(d).toLocaleDateString('zh-CN', { month: 'numeric', day: 'numeric' })
|
})
|
||||||
}
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
if (pollTimer) clearInterval(pollTimer)
|
stopPolling()
|
||||||
})
|
})
|
||||||
|
|
||||||
defineExpose({ open })
|
defineExpose({ open })
|
||||||
|
|
@ -404,20 +469,54 @@ defineExpose({ open })
|
||||||
}
|
}
|
||||||
|
|
||||||
.left-panel {
|
.left-panel {
|
||||||
|
position: relative;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
width: 677px;
|
width: 677px;
|
||||||
height: 493px;
|
height: 493px;
|
||||||
padding: 44px 46px;
|
padding: 44px 46px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
.custom-loading-mask {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(245, 245, 255, 0.75);
|
||||||
|
backdrop-filter: blur(2px);
|
||||||
|
border-radius: 20px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
z-index: 10;
|
||||||
|
color: #8B70FF;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
pointer-events: auto;
|
||||||
|
|
||||||
|
.el-icon {
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.plans-scroll {
|
.plans-scroll {
|
||||||
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
padding-bottom: 2px;
|
padding-bottom: 2px;
|
||||||
cursor: grab;
|
cursor: grab;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
border-radius: 16px;
|
||||||
|
|
||||||
|
&.is-disabled {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
&::-webkit-scrollbar {
|
&::-webkit-scrollbar {
|
||||||
display: none;
|
display: none;
|
||||||
|
|
@ -670,13 +769,40 @@ defineExpose({ open })
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
gap: 6px;
|
||||||
color: #999;
|
color: #999;
|
||||||
font-size: 14px;
|
font-size: 12px;
|
||||||
cursor: pointer;
|
|
||||||
background: #f5f5f5;
|
background: #f5f5f5;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
|
padding: 8px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
&.error {
|
||||||
|
cursor: pointer;
|
||||||
|
color: #ff4d4f;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #ffeef0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-icon {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.loading {
|
||||||
|
cursor: wait;
|
||||||
|
color: #8B70FF;
|
||||||
|
|
||||||
|
.el-icon {
|
||||||
|
font-size: 22px;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -736,4 +862,14 @@ defineExpose({ open })
|
||||||
right: -32px !important;
|
right: -32px !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.fade-enter-active,
|
||||||
|
.fade-leave-active {
|
||||||
|
transition: opacity 0.25s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-enter-from,
|
||||||
|
.fade-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
<div class="banner-content">
|
<div class="banner-content">
|
||||||
<span class="main-text">【 装样大师 】APP已上线!</span>
|
<span class="main-text">【 装样大师 】APP已上线!</span>
|
||||||
<span class="sub-text">各大应用市场均可下载</span>
|
<span class="sub-text">各大应用市场均可下载</span>
|
||||||
<img class="down-app-img" src="@/public/header/down-app-btn.png" alt="">
|
<img class="down-app-img" src="@/public/header/down-app-btn.png" alt="" @click="openAppCodeDialog">
|
||||||
</div>
|
</div>
|
||||||
<!-- <img class="close-icon" src="@/public/header/close.png" alt=""> -->
|
<!-- <img class="close-icon" src="@/public/header/close.png" alt=""> -->
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -38,6 +38,7 @@
|
||||||
<div class="right-box">
|
<div class="right-box">
|
||||||
<img v-if="!userStore.isVip && isLoggedIn" @click="handleCommand('vip')" class="vip-logo"
|
<img v-if="!userStore.isVip && isLoggedIn" @click="handleCommand('vip')" class="vip-logo"
|
||||||
src="@/public/header/kaitonghuiyuan.png" mode="heightFix" />
|
src="@/public/header/kaitonghuiyuan.png" mode="heightFix" />
|
||||||
|
<img v-else-if="isLoggedIn" class="vip-logo" src="@/public/header/zunguihuiyuan.png" mode="heightFix" />
|
||||||
<img v-if="!isLoggedIn" @click="openLoginDialog" class="vip-logo" src="@/public/header/login-btn.png"
|
<img v-if="!isLoggedIn" @click="openLoginDialog" class="vip-logo" src="@/public/header/login-btn.png"
|
||||||
mode="heightFix" />
|
mode="heightFix" />
|
||||||
<div v-else class="dropdown-wrapper">
|
<div v-else class="dropdown-wrapper">
|
||||||
|
|
@ -70,6 +71,11 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<LoginDialog ref="loginDialogRef" />
|
<LoginDialog ref="loginDialogRef" />
|
||||||
|
<BaseDialog ref="appCodeRef" v-model="showAppCode" width="260px" @close="showAppCode = false" title="下载APP">
|
||||||
|
<div class="qrCode-box">
|
||||||
|
<img style="width: 200px;height: 200px;margin: 0 auto;" :src="appCodeImg" alt="">
|
||||||
|
</div>
|
||||||
|
</BaseDialog>
|
||||||
</header>
|
</header>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
@ -78,6 +84,8 @@ import { computed, inject, ref, onMounted, watch } from 'vue'
|
||||||
import { useRouter, useRoute } from 'vue-router'
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
import { useUserStore } from '@/stores/user'
|
import { useUserStore } from '@/stores/user'
|
||||||
import LoginDialog from '@/components/LoginDialog.vue'
|
import LoginDialog from '@/components/LoginDialog.vue'
|
||||||
|
import BaseDialog from '@/components/common/BaseDialog.vue'
|
||||||
|
import authService from '@/api/authService.js'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
@ -85,6 +93,25 @@ const route = useRoute()
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
const showPayment = inject('showPayment')
|
const showPayment = inject('showPayment')
|
||||||
const loginDialogRef = ref(null)
|
const loginDialogRef = ref(null)
|
||||||
|
const appCodeRef = ref(null)
|
||||||
|
const showAppCode = ref(false)
|
||||||
|
const appCodeImg = ref('')
|
||||||
|
|
||||||
|
|
||||||
|
async function openAppCodeDialog() {
|
||||||
|
showAppCode.value = true
|
||||||
|
if (!appCodeImg.value) {
|
||||||
|
try {
|
||||||
|
const res = await authService.getAppCode()
|
||||||
|
console.log('获取APP二维码响应结果:', res)
|
||||||
|
const data = res.data || res
|
||||||
|
appCodeImg.value = data.qrcode || data.qr_code || data.qrcode_url || data.url || (typeof data === 'string' ? data : '')
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (localStorage.getItem('token')) {
|
if (localStorage.getItem('token')) {
|
||||||
|
|
@ -132,10 +159,14 @@ const navList = [
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{ name: '作品库', key: 'works', path: '/works' },
|
{ name: '作品库', key: 'works', path: '/works' },
|
||||||
{ name: '关于我们', key: 'about', path: '/about' }
|
{ name: '关于我们', key: 'about', path: '/about' },
|
||||||
|
{ name: '文章', key: 'article', path: '/article' }
|
||||||
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
function handleNavClick(item) {
|
function handleNavClick(item) {
|
||||||
|
if (isItemActive(item)) return
|
||||||
if (item.key === 'works' && !isLoggedIn.value) {
|
if (item.key === 'works' && !isLoggedIn.value) {
|
||||||
loginDialogRef.value?.open()
|
loginDialogRef.value?.open()
|
||||||
return
|
return
|
||||||
|
|
@ -147,6 +178,7 @@ function handleNavClick(item) {
|
||||||
|
|
||||||
function isItemActive(item) {
|
function isItemActive(item) {
|
||||||
if (item.path === route.path) return true
|
if (item.path === route.path) return true
|
||||||
|
if (item.key === 'article' && route.path.startsWith('/article')) return true
|
||||||
if (item.children) {
|
if (item.children) {
|
||||||
return item.children.some(child => child.path === route.path)
|
return item.children.some(child => child.path === route.path)
|
||||||
}
|
}
|
||||||
|
|
@ -526,4 +558,12 @@ function openLoginDialog() {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.qrCode-box {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 20px 0;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="base-dialog">
|
<div class="base-dialog">
|
||||||
|
|
||||||
<el-dialog class="base-dialog" v-model="visible" :width="width" align-center>
|
<el-dialog class="base-dialog" v-model="visible" :width="width" align-center>
|
||||||
<img src="@/public/header/close.png" class="close" @click="handleClose" />
|
<img src="@/public/header/close.png" class="close" @click="handleClose" />
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
|
|
@ -44,6 +43,11 @@ function handleClose() {
|
||||||
top: -34px;
|
top: -34px;
|
||||||
right: -34px;
|
right: -34px;
|
||||||
z-index: 9999;
|
z-index: 9999;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: scale(1.1);
|
||||||
|
transition: transform 0.3s;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-dialog__headerbtn {
|
.el-dialog__headerbtn {
|
||||||
|
|
@ -24,7 +24,7 @@
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
z-index: 100;
|
z-index: 1;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
|
||||||
.warning-icon {
|
.warning-icon {
|
||||||
|
|
@ -4,18 +4,24 @@
|
||||||
<div class="iphone-frame" :class="theme" id="capture-frame">
|
<div class="iphone-frame" :class="theme" id="capture-frame">
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
</div>
|
</div>
|
||||||
<div class="watermark-overlay"></div>
|
<div v-if="!isVip" class="watermark-overlay"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import { storeToRefs } from 'pinia'
|
||||||
|
import { useUserStore } from '@/stores/user'
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
theme: {
|
theme: {
|
||||||
type: String,
|
type: String,
|
||||||
default: 'light',
|
default: 'light',
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const userStore = useUserStore()
|
||||||
|
const { isVip } = storeToRefs(userStore)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { FolderAdd, Download } from '@element-plus/icons-vue'
|
import { FolderAdd, Download } from '@element-plus/icons-vue'
|
||||||
import WarningFooter from '@/components/WarningFooter.vue'
|
import WarningFooter from '@/components/common/WarningFooter.vue'
|
||||||
import PhoneMockup from '@/components/simulator/PhoneMockup.vue'
|
import PhoneMockup from '@/components/simulator/PhoneMockup.vue'
|
||||||
import StatusBar from '@/components/simulator/StatusBar.vue'
|
import StatusBar from '@/components/simulator/StatusBar.vue'
|
||||||
|
|
||||||
|
|
|
||||||
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 679 B |
|
After Width: | Height: | Size: 720 B |
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 997 B |
|
After Width: | Height: | Size: 970 B |
|
After Width: | Height: | Size: 530 B |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
|
@ -3,15 +3,13 @@ import { useUserStore } from '@/stores/user'
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
{ path: '/', name: 'index', component: () => import('@/views/index/index.vue') },
|
{ path: '/', name: 'index', component: () => import('@/views/index/index.vue') },
|
||||||
// { path: '/', name: 'Home', component: () => import('@/views/Home.vue') },
|
{ path: '/chat', name: 'Chat', component: () => import('@/views/wxPage/ChatEdit.vue'), meta: { requiresAuth: false } },
|
||||||
{ path: '/chat', name: 'Chat', component: () => import('@/views/ChatEdit.vue'), meta: { requiresAuth: false } },
|
{ path: '/balance', name: 'Balance', component: () => import('@/views/wxPage/BalanceEdit.vue'), meta: { requiresAuth: false } },
|
||||||
{ path: '/balance', name: 'Balance', component: () => import('@/views/BalanceEdit.vue'), meta: { requiresAuth: false } },
|
|
||||||
{ path: '/works', name: 'Works', component: () => import('@/views/work/works.vue'), meta: { requiresAuth: true } },
|
{ path: '/works', name: 'Works', component: () => import('@/views/work/works.vue'), meta: { requiresAuth: true } },
|
||||||
{ path: '/login', name: 'Login', component: () => import('@/views/Login.vue') },
|
|
||||||
{ path: '/register', name: 'Register', component: () => import('@/views/Register.vue') },
|
|
||||||
{ path: '/about', name: 'About', component: () => import('@/views/About.vue') },
|
{ path: '/about', name: 'About', component: () => import('@/views/About.vue') },
|
||||||
{ path: '/profile', name: 'Profile', component: () => import('@/views/Profile.vue'), meta: { requiresAuth: true } },
|
{ path: '/profile', name: 'Profile', component: () => import('@/views/Profile.vue'), meta: { requiresAuth: true } },
|
||||||
{ path: '/payment/result', name: 'PaymentResult', component: () => import('@/views/PaymentResult.vue') },
|
{ path: '/article', name: 'Article', component: () => import('@/views/article/list.vue') },
|
||||||
|
{ path: '/article-detail', name: 'ArticleDetail', component: () => import('@/views/article/detail.vue') },
|
||||||
{ path: '/gongzidan', name: 'GongZiDan', component: () => import('@/views/other/gongzidan.vue') },
|
{ path: '/gongzidan', name: 'GongZiDan', component: () => import('@/views/other/gongzidan.vue') },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ export const useUserStore = defineStore('user', () => {
|
||||||
const isVip = ref(false)
|
const isVip = ref(false)
|
||||||
const avatarUrl = ref('')
|
const avatarUrl = ref('')
|
||||||
const membershipExpireAt = ref(null)
|
const membershipExpireAt = ref(null)
|
||||||
|
const userInfoRaw = ref(null) // 响应式保存最新完整的用户原始数据
|
||||||
const tokenChecked = ref(false) // 是否已完成 Token 校验
|
const tokenChecked = ref(false) // 是否已完成 Token 校验
|
||||||
const tokenValid = ref(false) // Token 是否真实有效
|
const tokenValid = ref(false) // Token 是否真实有效
|
||||||
|
|
||||||
|
|
@ -20,6 +21,12 @@ export const useUserStore = defineStore('user', () => {
|
||||||
function setUser(data, fallbackPhone = '') {
|
function setUser(data, fallbackPhone = '') {
|
||||||
if (!data) return
|
if (!data) return
|
||||||
const userObj = data.user || data
|
const userObj = data.user || data
|
||||||
|
userInfoRaw.value = userObj
|
||||||
|
|
||||||
|
// 写入本地缓存,供快照加载与 checkUserVip 自动使用
|
||||||
|
try {
|
||||||
|
localStorage.setItem('user_info', JSON.stringify(userObj))
|
||||||
|
} catch (e) { }
|
||||||
|
|
||||||
// 游客账号特征判断:temp 为 true,或角色 role 为 '8',或名称包含 '游客'
|
// 游客账号特征判断:temp 为 true,或角色 role 为 '8',或名称包含 '游客'
|
||||||
const isGuest = userObj.temp === true || userObj.role === '8' || userObj.name === '游客' || userObj.nickname === '游客'
|
const isGuest = userObj.temp === true || userObj.role === '8' || userObj.name === '游客' || userObj.nickname === '游客'
|
||||||
|
|
@ -36,6 +43,7 @@ export const useUserStore = defineStore('user', () => {
|
||||||
username.value = userObj.name || '已登录用户'
|
username.value = userObj.name || '已登录用户'
|
||||||
isVip.value = checkUserVip(userObj)
|
isVip.value = checkUserVip(userObj)
|
||||||
avatarUrl.value = userObj.avater || userObj.avatar || ''
|
avatarUrl.value = userObj.avater || userObj.avatar || ''
|
||||||
|
membershipExpireAt.value = userObj.vip_expire || null
|
||||||
tokenChecked.value = true
|
tokenChecked.value = true
|
||||||
tokenValid.value = true
|
tokenValid.value = true
|
||||||
}
|
}
|
||||||
|
|
@ -46,7 +54,7 @@ export const useUserStore = defineStore('user', () => {
|
||||||
const token = localStorage.getItem('token')
|
const token = localStorage.getItem('token')
|
||||||
if (!token) {
|
if (!token) {
|
||||||
reset()
|
reset()
|
||||||
return
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1. 本地快照秒开:瞬间解析 user_info 填充响应式数据,0毫秒延迟
|
// 1. 本地快照秒开:瞬间解析 user_info 填充响应式数据,0毫秒延迟
|
||||||
|
|
@ -63,21 +71,22 @@ export const useUserStore = defineStore('user', () => {
|
||||||
return fetchPromise
|
return fetchPromise
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 后台非阻塞异步校验:将真实 API 鉴权放入后台处理
|
// 3. 实时后台接口校验:精准兼容多种 Api 返回数据结构并更新 store 与缓存
|
||||||
fetchPromise = (async () => {
|
fetchPromise = (async () => {
|
||||||
try {
|
try {
|
||||||
const res = await authService.getUserInfo()
|
const res = await authService.getUserInfo()
|
||||||
if (res && (res.code === 0 || res.data)) {
|
// 兼容 res.data 结构以及直接返回用户对象的结构
|
||||||
if (res.data) setUser(res.data)
|
const userData = (res && res.data) ? res.data : (res && (res.vip !== undefined || res.user_id || res.name) ? res : null)
|
||||||
console.log("用户信息", res.data)
|
|
||||||
return res.data
|
if (userData) {
|
||||||
} else {
|
setUser(userData)
|
||||||
// 如果后端明确返回 Token 过期/无效,清空登录态
|
console.log("最新后台用户信息已同步:", userData)
|
||||||
|
return userData
|
||||||
|
} else if (res && res.code && res.code !== 0) {
|
||||||
reset()
|
reset()
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Token 过期或鉴权失败,强制清空缓存并置为未登录
|
console.error("同步后台用户信息异常:", e)
|
||||||
reset()
|
|
||||||
} finally {
|
} finally {
|
||||||
fetchPromise = null
|
fetchPromise = null
|
||||||
}
|
}
|
||||||
|
|
@ -91,11 +100,12 @@ export const useUserStore = defineStore('user', () => {
|
||||||
isVip.value = false
|
isVip.value = false
|
||||||
avatarUrl.value = ''
|
avatarUrl.value = ''
|
||||||
membershipExpireAt.value = null
|
membershipExpireAt.value = null
|
||||||
|
userInfoRaw.value = null
|
||||||
tokenChecked.value = true
|
tokenChecked.value = true
|
||||||
tokenValid.value = false
|
tokenValid.value = false
|
||||||
localStorage.removeItem('token')
|
localStorage.removeItem('token')
|
||||||
localStorage.removeItem('user_info')
|
localStorage.removeItem('user_info')
|
||||||
}
|
}
|
||||||
|
|
||||||
return { username, isVip, avatarUrl, membershipExpireAt, tokenChecked, tokenValid, isLoggedIn, setUser, fetchProfile, reset, checkVip: checkUserVip }
|
return { username, isVip, avatarUrl, membershipExpireAt, userInfoRaw, tokenChecked, tokenValid, isLoggedIn, setUser, fetchProfile, reset, checkVip: checkUserVip }
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,27 +1,53 @@
|
||||||
|
import { useUserStore } from '@/stores/user'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 公共方法:根据用户信息判断用户是否为 VIP 会员
|
* 公共方法:根据用户信息判断用户是否为 VIP 会员
|
||||||
* @param {Object} userObj - 用户信息对象
|
* @param {Object} [userObj] - 用户信息对象(可选,未传时自动同步 Pinia/localStorage 数据)
|
||||||
* @returns {Boolean} - 是否为有效 VIP 会员
|
* @returns {Boolean} - 是否为有效 VIP 会员
|
||||||
*/
|
*/
|
||||||
export function checkUserVip(userObj) {
|
export function checkUserVip(userObj) {
|
||||||
if (!userObj || typeof userObj !== 'object') return false;
|
let targetUser = userObj;
|
||||||
|
|
||||||
|
// 0. 若未传入有效对象,优先从 userStore 的响应式数据读取,保证接口更新后 Vue 视图可响应式刷新
|
||||||
|
if (!targetUser || typeof targetUser !== 'object' || Object.keys(targetUser).length === 0) {
|
||||||
|
try {
|
||||||
|
const userStore = useUserStore();
|
||||||
|
if (userStore && userStore.userInfoRaw) {
|
||||||
|
targetUser = userStore.userInfoRaw;
|
||||||
|
}
|
||||||
|
} catch (e) { }
|
||||||
|
|
||||||
|
// 若 store 暂无数据,降级从 localStorage 读取缓存
|
||||||
|
if (!targetUser || typeof targetUser !== 'object' || Object.keys(targetUser).length === 0) {
|
||||||
|
try {
|
||||||
|
const localData = localStorage.getItem('user_info');
|
||||||
|
if (localData) {
|
||||||
|
targetUser = JSON.parse(localData);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('解析本地用户信息失败:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!targetUser || typeof targetUser !== 'object') return false;
|
||||||
|
|
||||||
// 1. 过滤游客/临时账号
|
// 1. 过滤游客/临时账号
|
||||||
if (userObj.temp === true || userObj.role === '8' || userObj.name === '游客' || userObj.nickname === '游客') {
|
if (targetUser.temp === true || targetUser.role === '8' || targetUser.name === '游客' || targetUser.nickname === '游客') {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. VIP 标记判定 (兼容 vip > 1、vip 为 true、is_vip 为 true 等字段结构)
|
// 2. VIP 标记判定 (兼容 vip > 1、vip 为 true、is_vip 为 true 等字段结构)
|
||||||
const isVipFlag = (typeof userObj.vip === 'number' ? userObj.vip > 1 : !!userObj.vip) ||
|
const isVipFlag = (typeof targetUser.vip === 'number' ? targetUser.vip > 1 : !!targetUser.vip) ||
|
||||||
!!userObj.is_vip ||
|
!!targetUser.is_vip ||
|
||||||
userObj.role === 'vip' ||
|
targetUser.role === 'vip' ||
|
||||||
userObj.role === '1';
|
targetUser.role === '1';
|
||||||
|
|
||||||
if (!isVipFlag) return false;
|
if (!isVipFlag) return false;
|
||||||
|
|
||||||
// 3. 校验过期时间 (如果包含 vip_expire)
|
// 3. 校验过期时间 (如果包含 vip_expire)
|
||||||
if (userObj.vip_expire) {
|
if (targetUser.vip_expire) {
|
||||||
const expireTime = new Date(userObj.vip_expire).getTime();
|
const expireTime = new Date(targetUser.vip_expire).getTime();
|
||||||
if (!isNaN(expireTime) && expireTime < Date.now()) {
|
if (!isNaN(expireTime) && expireTime < Date.now()) {
|
||||||
return false; // 已过期
|
return false; // 已过期
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -36,22 +36,18 @@ const setheader = (dataType) => {
|
||||||
} else {
|
} else {
|
||||||
header["Content-Type"] = "application/x-www-form-urlencoded;charset=UTF-8";
|
header["Content-Type"] = "application/x-www-form-urlencoded;charset=UTF-8";
|
||||||
}
|
}
|
||||||
|
|
||||||
header["x-channel"] = "official";
|
header["x-channel"] = "official";
|
||||||
header["x-version"] = "1.0.0";
|
header["x-version"] = "1.0.0";
|
||||||
header["x-platform"] = "pc";
|
header["x-platform"] = "web";
|
||||||
|
|
||||||
let deviceId = localStorage.getItem("device_id");
|
let deviceId = localStorage.getItem("device_id");
|
||||||
if (!deviceId) {
|
if (!deviceId) {
|
||||||
deviceId = appUtils.generateUUID();
|
deviceId = appUtils.generateUUID();
|
||||||
localStorage.setItem("device_id", deviceId);
|
localStorage.setItem("device_id", deviceId);
|
||||||
}
|
}
|
||||||
header["x-device-id"] = deviceId;
|
header["x-device-id"] = deviceId;
|
||||||
|
|
||||||
header["x-mobile-brand"] = "pc";
|
header["x-mobile-brand"] = "pc";
|
||||||
header["x-mobile-model"] = "pc";
|
header["x-mobile-model"] = "pc";
|
||||||
header["x-package"] = "com.devcon.flaunt";
|
header["x-package"] = "com.devcon.flaunt";
|
||||||
|
|
||||||
const token = localStorage.getItem("token");
|
const token = localStorage.getItem("token");
|
||||||
if (token) {
|
if (token) {
|
||||||
header["x-token"] = token;
|
header["x-token"] = token;
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import AppFooter from '@/components/AppFooter.vue'
|
import AppFooter from '@/components/common/AppFooter.vue'
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|
|
||||||
|
|
@ -1,128 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="home-page">
|
|
||||||
<!-- Hero Banner -->
|
|
||||||
<section class="hero">
|
|
||||||
<el-carousel :interval="4000" height="360px" v-if="banners.length">
|
|
||||||
<el-carousel-item v-for="b in banners" :key="b.id">
|
|
||||||
<div class="banner-item" :style="{ backgroundImage: `url(${b.image_url})` }" @click="goLink(b.link_url)">
|
|
||||||
<h2 class="banner-title">{{ b.title }}</h2>
|
|
||||||
</div>
|
|
||||||
</el-carousel-item>
|
|
||||||
</el-carousel>
|
|
||||||
<div class="hero-default" v-else>
|
|
||||||
<h1>微信截图生成器</h1>
|
|
||||||
<p>高度还原微信聊天界面 & 余额详情,所见即所得,一键导出高清截图</p>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- Feature Cards -->
|
|
||||||
<section class="features">
|
|
||||||
<div class="container">
|
|
||||||
<h2 class="section-title">核心功能</h2>
|
|
||||||
<div class="feature-cards">
|
|
||||||
<router-link to="/chat" class="feature-card">
|
|
||||||
<div class="card-icon">💬</div>
|
|
||||||
<h3>聊天记录模拟</h3>
|
|
||||||
<p>支持文字/红包/转账等多种消息类型,自定义头像昵称,像素级还原真实效果</p>
|
|
||||||
<el-button type="primary" plain>立即体验 →</el-button>
|
|
||||||
</router-link>
|
|
||||||
|
|
||||||
<router-link to="/balance" class="feature-card">
|
|
||||||
<div class="card-icon">💰</div>
|
|
||||||
<h3>微信余额模拟</h3>
|
|
||||||
<p>还原零钱页面,余额/零钱通/交易记录自由定制,支持深色主题切换</p>
|
|
||||||
<el-button type="primary" plain>立即体验 →</el-button>
|
|
||||||
</router-link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- How it works -->
|
|
||||||
<section class="steps-section">
|
|
||||||
<div class="container">
|
|
||||||
<h2 class="section-title">使用步骤</h2>
|
|
||||||
<div class="steps">
|
|
||||||
<div class="step"><span class="step-num">1</span><p>选择功能类型</p></div>
|
|
||||||
<div class="step-arrow">→</div>
|
|
||||||
<div class="step"><span class="step-num">2</span><p>编辑内容细节</p></div>
|
|
||||||
<div class="step-arrow">→</div>
|
|
||||||
<div class="step"><span class="step-num">3</span><p>实时预览效果</p></div>
|
|
||||||
<div class="step-arrow">→</div>
|
|
||||||
<div class="step"><span class="step-num">4</span><p>导出高清截图</p></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref, onMounted } from 'vue'
|
|
||||||
import api from '@/api'
|
|
||||||
|
|
||||||
const banners = ref([])
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
try { banners.value = await api.public.banners() } catch {}
|
|
||||||
})
|
|
||||||
|
|
||||||
function goLink(url) { if (url) window.open(url, '_blank') }
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.home-page { background: #fff; }
|
|
||||||
|
|
||||||
.hero {
|
|
||||||
.banner-item {
|
|
||||||
width: 100%; height: 100%;
|
|
||||||
background-size: cover; background-position: center;
|
|
||||||
display: flex; align-items: center; justify-content: center; cursor: pointer;
|
|
||||||
.banner-title { color: #fff; font-size: 28px; text-shadow: 0 1px 4px rgba(0,0,0,.3); }
|
|
||||||
}
|
|
||||||
.hero-default {
|
|
||||||
height: 360px; display: flex; flex-direction: column;
|
|
||||||
align-items: center; justify-content: center;
|
|
||||||
background: linear-gradient(135deg, #07c160 0%, #06ad56 100%);
|
|
||||||
color: #fff;
|
|
||||||
h1 { font-size: 36px; margin-bottom: 12px; }
|
|
||||||
p { font-size: 16px; opacity: .9; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.features { padding: 60px 0; background: #f8f9fa; }
|
|
||||||
|
|
||||||
.container { max-width: 1100px; margin: 0 auto; padding: 0 20px; }
|
|
||||||
|
|
||||||
.section-title {
|
|
||||||
text-align: center; font-size: 28px; font-weight: bold; margin-bottom: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.feature-cards {
|
|
||||||
display: grid; grid-template-columns: repeat(2, 1fr); gap: 30px;
|
|
||||||
|
|
||||||
.feature-card {
|
|
||||||
background: #fff; border-radius: 16px; padding: 40px 32px;
|
|
||||||
text-align: center; box-shadow: 0 4px 20px rgba(0,0,0,.06);
|
|
||||||
transition: transform .25s, box-shadow .25s;
|
|
||||||
&:hover { transform: translateY(-4px); box-shadow: 0 8px 30px rgba(0,0,0,.10); }
|
|
||||||
.card-icon { font-size: 48px; margin-bottom: 16px; }
|
|
||||||
h3 { font-size: 20px; margin-bottom: 10px; color: #333; }
|
|
||||||
p { color: #666; line-height: 1.7; margin-bottom: 20px; font-size: 14px; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.steps-section { padding: 50px 0; }
|
|
||||||
.steps {
|
|
||||||
display: flex; align-items: center; justify-content: center; gap: 16px;
|
|
||||||
.step {
|
|
||||||
display: flex; flex-direction: column; align-items: center; gap: 8px;
|
|
||||||
.step-num {
|
|
||||||
width: 44px; height: 44px; border-radius: 50%;
|
|
||||||
background: #07c160; color: #fff;
|
|
||||||
display: flex; align-items: center; justify-content: center;
|
|
||||||
font-size: 18px; font-weight: bold;
|
|
||||||
}
|
|
||||||
p { font-size: 13px; color: #666; }
|
|
||||||
}
|
|
||||||
.step-arrow { font-size: 24px; color: #ccc; }
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,174 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="auth-page">
|
|
||||||
<div class="auth-card">
|
|
||||||
<h2>账号登录</h2>
|
|
||||||
<el-form :model="form" :rules="rules" ref="formRef" @keyup.enter="handleLogin">
|
|
||||||
<el-form-item prop="username">
|
|
||||||
<el-input v-model="form.username" placeholder="用户名或邮箱" prefix-icon="User" size="large" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item prop="password">
|
|
||||||
<el-input v-model="form.password" type="password" placeholder="密码" prefix-icon="Lock" size="large" show-password />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item prop="captcha">
|
|
||||||
<div style="display:flex;gap:10px;width:100%">
|
|
||||||
<el-input v-model="form.captcha" placeholder="验证码(不区分大小写)" size="large" style="flex:1" />
|
|
||||||
<div class="captcha-img" @click="loadCaptcha" title="点击刷新">
|
|
||||||
<img v-if="captchaData.image" :src="captchaData.image" alt="" />
|
|
||||||
<span v-else-if="captchaData.text_mode" class="captcha-text">点击刷新</span>
|
|
||||||
<span v-else class="captcha-text loading-text">加载中...</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item>
|
|
||||||
<el-button type="primary" size="large" :loading="loading" style="width:100%" @click="handleLogin">登 录</el-button>
|
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<!-- 微信扫码登录 -->
|
|
||||||
<el-divider><span style="color:#999;font-size:12px">其他登录方式</span></el-divider>
|
|
||||||
<div class="wechat-login-area">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="wechat-login-btn"
|
|
||||||
:disabled="wechatLoading"
|
|
||||||
@click="handleWechatLogin"
|
|
||||||
>
|
|
||||||
<svg viewBox="0 0 24 24" fill="#07c160" width="20" height="20"><path d="M8.691 2.188C3.891 2.188 0 5.476 0 9.53c0 2.212 1.17 4.203 3.002 5.55a.59.59 0 01.213.665l-.39 1.48c-.019.07-.048.141-.048.213 0 .163.13.295.29.295a.326.326 0 00.167-.054l1.903-1.114a.864.864 0 01.717-.098 10.16 10.16 0 002.837.403c.276 0 .543-.027.811-.05-.857-2.578.157-4.972 1.932-6.446 1.703-1.415 3.882-1.98 5.853-1.838-.576-3.583-4.196-6.348-8.596-6.348zM5.785 5.991c.642 0 1.162.529 1.162 1.18a1.17 1.17 0 01-1.162 1.178A1.17 1.17 0 014.623 7.17c0-.651.52-1.18 1.162-1.18zm5.813 0c.642 0 1.162.529 1.162 1.18a1.17 1.17 0 01-1.162 1.178 1.17 1.17 0 01-1.162-1.178c0-.651.52-1.18 1.162-1.18zm5.34 2.867c-1.797-.052-3.746.512-5.28 1.786-1.72 1.428-2.687 3.72-1.78 6.22.942 2.453 3.666 4.229 6.884 4.229.826 0 1.622-.12 2.361-.343a.722.722 0 01.598.082l1.584.926a.272.272 0 00.14.045c.134 0 .24-.111.24-.245 0-.06-.023-.118-.038-.177l-.327-1.233a.582.582 0 01-.023-.156.49.49 0 01.201-.398C23.024 18.48 24 16.82 24 14.98c0-3.21-2.931-5.837-6.656-6.088V8.89l-.007-.03zm-2.574 2.74c.535 0 .969.44.969.982a.976.976 0 01-.969.983.976.976 0 01-.969-.983c0-.542.434-.982.97-.982zm4.844 0c.535 0 .969.44.969.982a.976.976 0 01-.97.983.976.976 0 01-.968-.983c0-.542.434-.982.969-.982z"/></svg>
|
|
||||||
{{ wechatLoading ? '获取中...' : '微信扫码登录' }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</el-form>
|
|
||||||
<p class="switch-text">
|
|
||||||
还没有账号?<router-link to="/register">立即注册</router-link>
|
|
||||||
</p>
|
|
||||||
<p class="forgot-text">忘记密码?请联系管理员重置</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref, reactive, onMounted } from 'vue'
|
|
||||||
import { useRouter, useRoute } from 'vue-router'
|
|
||||||
import { ElMessage } from 'element-plus'
|
|
||||||
import api from '@/api'
|
|
||||||
import { useUserStore } from '@/stores/user'
|
|
||||||
|
|
||||||
const router = useRouter()
|
|
||||||
const route = useRoute()
|
|
||||||
const userStore = useUserStore()
|
|
||||||
const formRef = ref()
|
|
||||||
const loading = ref(false)
|
|
||||||
const wechatLoading = ref(false)
|
|
||||||
const captchaKey = ref('')
|
|
||||||
const captchaData = ref({ image: '', text_mode: false })
|
|
||||||
|
|
||||||
// 已登录则跳转
|
|
||||||
if (localStorage.getItem('token')) {
|
|
||||||
router.replace(route.query.redirect || '/')
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检测微信回调返回的 token(从 URL 参数)
|
|
||||||
if (route.query.wechat_login && route.query.token) {
|
|
||||||
localStorage.setItem('token', route.query.token)
|
|
||||||
// 清理 URL 参数
|
|
||||||
const cleanUrl = window.location.pathname + window.location.hash.split('?')[0]
|
|
||||||
window.history.replaceState({}, document.title, cleanUrl)
|
|
||||||
// 获取用户信息并跳转
|
|
||||||
userStore.fetchProfile().then(() => {
|
|
||||||
ElMessage.success('微信登录成功')
|
|
||||||
router.push(route.query.redirect || '/')
|
|
||||||
}).catch(() => {
|
|
||||||
// token 无效则清除
|
|
||||||
localStorage.removeItem('token')
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const form = reactive({ username: '', password: '', captcha: '' })
|
|
||||||
const rules = {
|
|
||||||
username: [{ required: true, message: '请输入账号', trigger: 'blur' }],
|
|
||||||
password: [{ required: true, message: '请输入密码', trigger: 'blur' }],
|
|
||||||
captcha: [{ required: true, message: '请输入验证码', trigger: 'blur' }],
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => loadCaptcha())
|
|
||||||
|
|
||||||
async function handleWechatLogin() {
|
|
||||||
wechatLoading.value = true
|
|
||||||
try {
|
|
||||||
const res = await api.auth.wechatLoginUrl()
|
|
||||||
if (res.oauth_url) {
|
|
||||||
window.location.href = res.oauth_url
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
ElMessage.warning('微信登录暂不可用,请联系管理员')
|
|
||||||
} finally {
|
|
||||||
wechatLoading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadCaptcha() {
|
|
||||||
try {
|
|
||||||
const data = await api.auth.captcha()
|
|
||||||
captchaData.value = data
|
|
||||||
captchaKey.value = data.captcha_key || ''
|
|
||||||
} catch { ElMessage.warning('验证码加载失败,请点击刷新') }
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleLogin() {
|
|
||||||
const valid = await formRef.value.validate().catch(() => false)
|
|
||||||
if (!valid) return
|
|
||||||
loading.value = true
|
|
||||||
try {
|
|
||||||
const payload = { ...form, captcha_key: captchaKey.value }
|
|
||||||
const res = await api.auth.login(payload)
|
|
||||||
localStorage.setItem('token', res.token)
|
|
||||||
userStore.username = res.user.username
|
|
||||||
userStore.isVip = res.user.is_vip
|
|
||||||
userStore.tokenChecked = true
|
|
||||||
userStore.tokenValid = true
|
|
||||||
ElMessage.success('登录成功')
|
|
||||||
router.push(route.query.redirect || '/')
|
|
||||||
} catch (err) {
|
|
||||||
const msg = err?.response?.data?.error || '登录失败,请稍后重试'
|
|
||||||
ElMessage.error(msg)
|
|
||||||
loadCaptcha()
|
|
||||||
form.captcha = ''
|
|
||||||
} finally { loading.value = false }
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.auth-page {
|
|
||||||
min-height: calc(100vh - 120px);
|
|
||||||
display: flex; align-items: center; justify-content: center;
|
|
||||||
padding: 40px 20px;
|
|
||||||
}
|
|
||||||
.auth-card {
|
|
||||||
width: 420px; padding: 40px; border-radius: 12px;
|
|
||||||
background: #fff; box-shadow: 0 8px 32px rgba(0,0,0,.08);
|
|
||||||
h2 { text-align: center; margin-bottom: 28px; font-size: 22px; }
|
|
||||||
.captcha-img {
|
|
||||||
width: 120px; height: 40px; border: 1px solid #dcdfe6; border-radius: 4px;
|
|
||||||
cursor: pointer; overflow: hidden; display: flex; align-items: center; justify-content: center;
|
|
||||||
img { width: 100%; height: 100%; object-fit: cover; }
|
|
||||||
.captcha-text { font-family: monospace; font-size: 18px; letter-spacing: 6px; font-weight: bold; color: #333; }
|
|
||||||
.loading-text { font-size: 12px; letter-spacing: normal; color: #999; }
|
|
||||||
}
|
|
||||||
.switch-text { text-align: center; color: #999; font-size: 13px; a { color: #07c160; } }
|
|
||||||
.forgot-text { text-align: center; color: #bbb; font-size: 12px; margin-top: 4px; }
|
|
||||||
:deep(.el-divider__text) { background-color: transparent; }
|
|
||||||
|
|
||||||
.wechat-login-area {
|
|
||||||
display: flex; justify-content: center; padding: 8px 0;
|
|
||||||
}
|
|
||||||
.wechat-login-btn {
|
|
||||||
display:flex; align-items:center; gap:6px; padding:10px 32px;
|
|
||||||
border:1px solid #e0e0e0; border-radius:22px;
|
|
||||||
background:#fff; cursor:pointer; font-size:14px; color:#333;
|
|
||||||
transition:all .2s; outline:none;
|
|
||||||
}
|
|
||||||
.wechat-login-btn:hover:not(:disabled) {
|
|
||||||
border-color:#07c160; color:#07c160; background:#f0faf0;
|
|
||||||
}
|
|
||||||
.wechat-login-btn:disabled { opacity:.5; cursor:not-allowed; }
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,142 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="payment-result-page">
|
|
||||||
<div class="result-card" :class="statusClass">
|
|
||||||
<div class="result-icon">{{ iconText }}</div>
|
|
||||||
<h2 class="result-title">{{ title }}</h2>
|
|
||||||
<p class="result-desc">{{ desc }}</p>
|
|
||||||
|
|
||||||
<!-- 订单信息 -->
|
|
||||||
<template v-if="orderNo">
|
|
||||||
<div class="order-info">
|
|
||||||
<div class="info-row">
|
|
||||||
<span class="info-label">订单编号</span>
|
|
||||||
<span class="info-value mono">{{ orderNo }}</span>
|
|
||||||
</div>
|
|
||||||
<div v-if="tradeNo" class="info-row">
|
|
||||||
<span class="info-label">支付宝交易号</span>
|
|
||||||
<span class="info-value mono">{{ tradeNo }}</span>
|
|
||||||
</div>
|
|
||||||
<div v-if="amount" class="info-row">
|
|
||||||
<span class="info-label">支付金额</span>
|
|
||||||
<span class="info-value amount">¥{{ amount }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<div class="result-actions">
|
|
||||||
<el-button type="primary" size="large" @click="goHome">返回首页</el-button>
|
|
||||||
<el-button size="large" @click="goProfile">查看订单</el-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref, computed, onMounted } from 'vue'
|
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
|
||||||
import api from '@/api'
|
|
||||||
|
|
||||||
const route = useRoute()
|
|
||||||
const router = useRouter()
|
|
||||||
|
|
||||||
const orderNo = ref(route.query.out_trade_no || '')
|
|
||||||
const tradeNo = ref(route.query.trade_no || '')
|
|
||||||
const amount = ref(route.query.total_amount || '')
|
|
||||||
const payStatus = ref('') // 'success' | 'pending' | 'failed'
|
|
||||||
|
|
||||||
const statusClass = computed(() => {
|
|
||||||
if (payStatus.value === 'success') return 'card-success'
|
|
||||||
if (payStatus.value === 'failed') return 'card-failed'
|
|
||||||
return 'card-pending'
|
|
||||||
})
|
|
||||||
|
|
||||||
const iconText = computed(() => {
|
|
||||||
if (payStatus.value === 'success') return '✅'
|
|
||||||
if (payStatus.value === 'failed') return '❌'
|
|
||||||
return '⏳'
|
|
||||||
})
|
|
||||||
|
|
||||||
const title = computed(() => {
|
|
||||||
if (payStatus.value === 'success') return '支付成功'
|
|
||||||
if (payStatus.value === 'failed') return '支付失败'
|
|
||||||
return '确认中...'
|
|
||||||
})
|
|
||||||
|
|
||||||
const desc = computed(() => {
|
|
||||||
if (payStatus.value === 'success') return '会员已开通,感谢您的购买!'
|
|
||||||
if (payStatus.value === 'failed') return '支付未完成,请稍后在我的订单中查看'
|
|
||||||
return '正在确认支付结果,请稍候...'
|
|
||||||
})
|
|
||||||
|
|
||||||
async function verifyOrder() {
|
|
||||||
// 支付宝同步跳转带 trade_status 参数
|
|
||||||
const qsTradeStatus = route.query.trade_status
|
|
||||||
|
|
||||||
if (qsTradeStatus === 'TRADE_SUCCESS' || qsTradeStatus === 'TRADE_FINISHED') {
|
|
||||||
payStatus.value = 'success'
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果有订单号,去后端查询真实状态
|
|
||||||
if (orderNo.value) {
|
|
||||||
try {
|
|
||||||
const res = await api.payment.checkResult(orderNo.value)
|
|
||||||
payStatus.value = res.status === 'paid' ? 'success' : 'pending'
|
|
||||||
} catch {
|
|
||||||
// 接口不可用时根据支付宝回调参数判断
|
|
||||||
payStatus.value = qsTradeStatus ? 'failed' : 'pending'
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
payStatus.value = 'pending'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function goHome() {
|
|
||||||
router.push('/')
|
|
||||||
}
|
|
||||||
|
|
||||||
function goProfile() {
|
|
||||||
router.push('/profile')
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
verifyOrder()
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.payment-result-page {
|
|
||||||
display:flex; align-items:center; justify-content:center;
|
|
||||||
min-height:80vh; padding:20px; background:#f5f7fa;
|
|
||||||
}
|
|
||||||
.result-card {
|
|
||||||
width:460px; max-width:90vw; border-radius:16px;
|
|
||||||
padding:40px 32px 32px; text-align:center;
|
|
||||||
box-shadow:0 8px 30px rgba(0,0,0,.08);
|
|
||||||
}
|
|
||||||
.card-success { background:#fff; border-top:4px solid #52c41a; }
|
|
||||||
.card-failed { background:#fff; border-top:4px solid #ff4d4f; }
|
|
||||||
.card-pending { background:#fff; border-top:4px solid #1890ff; }
|
|
||||||
|
|
||||||
.result-icon { font-size:56px; margin-bottom:16px; }
|
|
||||||
.result-title { font-size:22px; font-weight:600; color:#333; margin:0 0 10px; }
|
|
||||||
.result-desc { font-size:14px; color:#888; margin:0 0 24px; line-height:1.6; }
|
|
||||||
|
|
||||||
.order-info {
|
|
||||||
text-align:left; background:#fafbfc; border-radius:10px;
|
|
||||||
padding:16px 20px; margin-bottom:24px;
|
|
||||||
}
|
|
||||||
.info-row {
|
|
||||||
display:flex; justify-content:space-between; align-items:center;
|
|
||||||
padding:8px 0; border-bottom:1px solid #f0f0f0;
|
|
||||||
}
|
|
||||||
.info-row:last-child { border-bottom:none; }
|
|
||||||
.info-label { color:#999; font-size:13px; }
|
|
||||||
.info-value { color:#333; font-size:13px; font-weight:500; }
|
|
||||||
.mono { font-family:'Courier New',Consolas,monospace; word-break:break-all; }
|
|
||||||
.amount { color:#ff4d4f; font-size:18px !important; font-weight:700; }
|
|
||||||
|
|
||||||
.result-actions {
|
|
||||||
display:flex; gap:12px; justify-content:center;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -6,18 +6,20 @@
|
||||||
<div class="profile-header">
|
<div class="profile-header">
|
||||||
<div class="left-info">
|
<div class="left-info">
|
||||||
<div class="avatar-box">
|
<div class="avatar-box">
|
||||||
<el-avatar :size="80" :src="userInfo.avatar_url || undefined">
|
<el-avatar :size="80"
|
||||||
{{ userInfo.username?.charAt(0)?.toUpperCase() || 'U' }}
|
:src="userInfo.avater || userInfo.avatar || userInfo.avatar_url || userStore.avatarUrl || undefined">
|
||||||
|
{{ (userInfo.name || userInfo.username || userStore.username || 'U').charAt(0).toUpperCase() }}
|
||||||
</el-avatar>
|
</el-avatar>
|
||||||
</div>
|
</div>
|
||||||
<div class="info-content">
|
<div class="info-content">
|
||||||
<div class="name-row">
|
<div class="name-row">
|
||||||
<span class="name">{{ userInfo.username || '用户' }}</span>
|
<span class="name">{{ userInfo.name || userInfo.username || userStore.username || '用户' }}</span>
|
||||||
<div v-if="checkUserVip(userInfo)" class="vip-tag">尊贵会员</div>
|
<div v-if="userStore.isVip" class="vip-tag">尊贵会员</div>
|
||||||
<div v-else class="normal-tag">普通用户</div>
|
<div v-else class="normal-tag">普通用户</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="email">
|
<div class="email">
|
||||||
<span v-if="checkUserVip(userInfo)">会员到期:{{ userInfo.vip_expire }}</span>
|
<span v-if="userStore.isVip">会员到期:{{ userInfo.vip_expire || userStore.membershipExpireAt || '生效中'
|
||||||
|
}}</span>
|
||||||
<span v-else>开通会员解锁全部功能</span>
|
<span v-else>开通会员解锁全部功能</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -53,14 +55,14 @@
|
||||||
<div v-if="orders.length > 0" class="order-list">
|
<div v-if="orders.length > 0" class="order-list">
|
||||||
<div v-for="o in orders.slice(0, 3)" :key="o.id" class="order-item">
|
<div v-for="o in orders.slice(0, 3)" :key="o.id" class="order-item">
|
||||||
<div class="order-left">
|
<div class="order-left">
|
||||||
<span class="order-plan">{{ planLabel(o.plan_type) }}</span>
|
<span class="order-plan">{{ planLabel(o.goods_name) }}</span>
|
||||||
<span class="order-time">{{ formatDate(o.created_at) }}</span>
|
<span class="order-time">{{ formatDate(o.pay_time) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="order-right">
|
<div class="order-right">
|
||||||
<span class="order-amount"><span style="font-size: 12px;">¥</span>{{ o.amount }}</span>
|
<span class="order-amount"><span style="font-size: 12px;">¥</span>{{ o.total_fee }}</span>
|
||||||
<el-tag :type="o.status === '已过期' ? 'info' : 'success'" size="small">
|
<!-- <el-tag :type="o.status === '已过期' ? 'info' : 'success'" size="small">
|
||||||
{{ statusLabel(o.status) }}
|
{{ statusLabel(o.status) }}
|
||||||
</el-tag>
|
</el-tag> -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -83,14 +85,14 @@
|
||||||
<div class="orders-list">
|
<div class="orders-list">
|
||||||
<div v-for="(o, index) in orders" :key="index" class="order-item">
|
<div v-for="(o, index) in orders" :key="index" class="order-item">
|
||||||
<div class="order-left">
|
<div class="order-left">
|
||||||
<span class="order-plan">{{ planLabel(o.plan_type) }}</span>
|
<span class="order-plan">{{ planLabel(o.goods_name) }}</span>
|
||||||
<span class="order-time">{{ formatDate(o.created_at) }}</span>
|
<span class="order-time">{{ formatDate(o.pay_time) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="order-right">
|
<div class="order-right">
|
||||||
<span class="order-amount"><span class="symbol">¥</span>{{ o.amount }}</span>
|
<span class="order-amount"><span class="symbol">¥</span>{{ o.total_fee }}</span>
|
||||||
<el-tag :type="o.status === '已过期' ? 'info' : 'success'" size="small">
|
<!-- <el-tag :type="o.status === '已过期' ? 'info' : 'success'" size="small">
|
||||||
{{ statusLabel(o.status) }}
|
{{ statusLabel(o.status) }}
|
||||||
</el-tag>
|
</el-tag> -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -107,8 +109,9 @@ import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import api from '@/api'
|
import api from '@/api'
|
||||||
import { useUserStore } from '@/stores/user'
|
import { useUserStore } from '@/stores/user'
|
||||||
import { checkUserVip } from '@/utils/appUtils'
|
import { checkUserVip } from '@/utils/appUtils'
|
||||||
import BaseDialog from '@/components/BaseDialog.vue'
|
import BaseDialog from '@/components/common/BaseDialog.vue'
|
||||||
import WarningFooter from '@/components/WarningFooter.vue'
|
import WarningFooter from '@/components/common/WarningFooter.vue'
|
||||||
|
import authService from '@/api/authService.js'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
|
|
@ -119,38 +122,7 @@ const userInfo = reactive({
|
||||||
membership_expire_at: null, created_at: ''
|
membership_expire_at: null, created_at: ''
|
||||||
})
|
})
|
||||||
|
|
||||||
const defaultOrder = [
|
const defaultOrder = []
|
||||||
{
|
|
||||||
plan_type: '月卡会员',
|
|
||||||
created_at: '2026.6.1-2026.7.1',
|
|
||||||
amount: '19.90',
|
|
||||||
status: '进行中'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
plan_type: '月卡会员',
|
|
||||||
created_at: '2026.1.1-2026.2.1',
|
|
||||||
amount: '19.90',
|
|
||||||
status: '已过期'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
plan_type: '月卡会员',
|
|
||||||
created_at: '2026.1.1-2026.2.1',
|
|
||||||
amount: '19.90',
|
|
||||||
status: '已过期'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
plan_type: '月卡会员',
|
|
||||||
created_at: '2026.1.1-2026.2.1',
|
|
||||||
amount: '19.90',
|
|
||||||
status: '已过期'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
plan_type: '月卡会员',
|
|
||||||
created_at: '2026.1.1-2026.2.1',
|
|
||||||
amount: '19.90',
|
|
||||||
status: '已过期'
|
|
||||||
},
|
|
||||||
]
|
|
||||||
const isVip = ref(false)
|
const isVip = ref(false)
|
||||||
const dialogVisible = ref(false)
|
const dialogVisible = ref(false)
|
||||||
const worksCount = ref(1)
|
const worksCount = ref(1)
|
||||||
|
|
@ -158,26 +130,35 @@ const ordersCount = ref(0)
|
||||||
const orders = ref(defaultOrder)
|
const orders = ref(defaultOrder)
|
||||||
const productMap = ref({}) // productId -> productName 映射
|
const productMap = ref({}) // productId -> productName 映射
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(async () => {
|
||||||
// 优先从 userStore 读取(注册/登录时已设置),立即显示页面
|
try {
|
||||||
if (userStore.username) {
|
const data = await userStore.fetchProfile()
|
||||||
userInfo.username = userStore.username
|
if (data) {
|
||||||
userInfo.avatar_url = userStore.avatarUrl || ''
|
Object.assign(userInfo, data)
|
||||||
isVip.value = userStore.isVip
|
}
|
||||||
// 静默请求补充 email/created_at 等字段,失败不弹提示
|
} catch (e) { }
|
||||||
api.auth.profileSilent().then(res => {
|
|
||||||
Object.assign(userInfo, res)
|
// 从本地缓存补全 userInfo 视图属性
|
||||||
isVip.value = !!res.is_vip
|
try {
|
||||||
}).catch(() => { })
|
const local = JSON.parse(localStorage.getItem('user_info') || '{}')
|
||||||
} else {
|
Object.assign(userInfo, local)
|
||||||
api.auth.profileSilent().then(res => {
|
} catch (e) { }
|
||||||
Object.assign(userInfo, res)
|
|
||||||
isVip.value = !!res.is_vip
|
loadStats();
|
||||||
}).catch(() => { })
|
getOrders();
|
||||||
}
|
|
||||||
loadStats()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
async function getOrders() {
|
||||||
|
try {
|
||||||
|
const res = await authService.getOrder({ status: 2 })
|
||||||
|
if (res.code == 0 && res.data) {
|
||||||
|
console.log("订单列表", res.data)
|
||||||
|
orders.value = res.data
|
||||||
|
ordersCount.value = res.data.length
|
||||||
|
}
|
||||||
|
} catch (error) { }
|
||||||
|
}
|
||||||
|
|
||||||
async function loadStats() {
|
async function loadStats() {
|
||||||
// try {
|
// try {
|
||||||
// const [wRes, oRes, pRes] = await Promise.all([
|
// const [wRes, oRes, pRes] = await Promise.all([
|
||||||
|
|
|
||||||
|
|
@ -1,168 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="auth-page">
|
|
||||||
<div class="auth-card">
|
|
||||||
<h2>注册新账号</h2>
|
|
||||||
<el-form :model="form" :rules="rules" ref="formRef" @keyup.enter="handleRegister">
|
|
||||||
<el-form-item prop="username">
|
|
||||||
<el-input v-model="form.username" placeholder="用户名(3-20位字母数字下划线)" prefix-icon="User" size="large" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item prop="email">
|
|
||||||
<el-input v-model="form.email" placeholder="邮箱" prefix-icon="Message" size="large" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item prop="password">
|
|
||||||
<el-input v-model="form.password" type="password" placeholder="密码(至少8位)" prefix-icon="Lock" size="large" show-password @input="checkStrength" />
|
|
||||||
<div v-if="form.password" class="pwd-strength">
|
|
||||||
<span :class="['strength-bar', strengthLevel]"></span>
|
|
||||||
<span class="strength-label">{{ strengthLabel }}</span>
|
|
||||||
</div>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item prop="confirmPassword">
|
|
||||||
<el-input v-model="form.confirmPassword" type="password" placeholder="确认密码" prefix-icon="Lock" size="large" show-password />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item prop="captcha">
|
|
||||||
<div style="display:flex;gap:10px;width:100%">
|
|
||||||
<el-input v-model="form.captcha" placeholder="验证码(不区分大小写)" size="large" style="flex:1" />
|
|
||||||
<div class="captcha-img" @click="loadCaptcha" title="点击刷新">
|
|
||||||
<img v-if="captchaData.image" :src="captchaData.image" alt="" />
|
|
||||||
<span v-else-if="captchaData.text_mode" class="captcha-text">点击刷新</span>
|
|
||||||
<span v-else class="captcha-text loading-text">加载中...</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item>
|
|
||||||
<el-button type="primary" size="large" :loading="loading" style="width:100%" @click="handleRegister">注 册</el-button>
|
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
|
||||||
<p class="switch-text">已有账号?<router-link to="/login">去登录</router-link></p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref, reactive, computed, onMounted } from 'vue'
|
|
||||||
import { useRouter } from 'vue-router'
|
|
||||||
import { ElMessage } from 'element-plus'
|
|
||||||
import api from '@/api'
|
|
||||||
import { useUserStore } from '@/stores/user'
|
|
||||||
|
|
||||||
const router = useRouter()
|
|
||||||
const userStore = useUserStore()
|
|
||||||
const formRef = ref()
|
|
||||||
const loading = ref(false)
|
|
||||||
const captchaKey = ref('')
|
|
||||||
const captchaData = ref({ image: '', text_mode: false })
|
|
||||||
|
|
||||||
const form = reactive({ username: '', email: '', password: '', confirmPassword: '', captcha: '' })
|
|
||||||
const pwdScore = ref(0)
|
|
||||||
function checkStrength() {
|
|
||||||
const p = form.password
|
|
||||||
let s = 0
|
|
||||||
if (p.length >= 8) s++
|
|
||||||
if (/[a-z]/.test(p)) s++
|
|
||||||
if (/[A-Z]/.test(p)) s++
|
|
||||||
if (/\d/.test(p)) s++
|
|
||||||
if (/[^a-zA-Z\d]/.test(p)) s++
|
|
||||||
// 额外长度加分
|
|
||||||
if (p.length >= 12) s++
|
|
||||||
pwdScore.value = s
|
|
||||||
}
|
|
||||||
|
|
||||||
const strengthLevel = computed(() => {
|
|
||||||
if (pwdScore.value <= 2) return 'weak'
|
|
||||||
if (pwdScore.value <= 4) return 'medium'
|
|
||||||
return 'strong'
|
|
||||||
})
|
|
||||||
const strengthLabel = computed(() => {
|
|
||||||
const map = { weak: '弱', medium: '中', strong: '强' }
|
|
||||||
return `密码强度:${map[strengthLevel.value]}` || ''
|
|
||||||
})
|
|
||||||
|
|
||||||
const validateConfirm = (rule, value, cb) => {
|
|
||||||
if (value !== form.password) cb(new Error('两次密码不一致'))
|
|
||||||
else cb()
|
|
||||||
}
|
|
||||||
const rules = {
|
|
||||||
username: [
|
|
||||||
{ required: true, message: '请输入用户名', trigger: 'blur' },
|
|
||||||
{ min: 3, max: 20, message: '3-20位字符', trigger: 'blur' },
|
|
||||||
],
|
|
||||||
email: [
|
|
||||||
{ required: true, message: '请输入邮箱', trigger: 'blur' },
|
|
||||||
{ type: 'email', message: '邮箱格式不正确', trigger: 'blur' },
|
|
||||||
],
|
|
||||||
password: [
|
|
||||||
{ required: true, message: '请输入密码', trigger: 'blur' },
|
|
||||||
{ min: 8, message: '至少8位', trigger: 'blur' },
|
|
||||||
],
|
|
||||||
confirmPassword: [{ validator: validateConfirm, trigger: 'blur' }],
|
|
||||||
captcha: [{ required: true, message: '请输入验证码', trigger: 'blur' }],
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(loadCaptcha)
|
|
||||||
|
|
||||||
async function loadCaptcha() {
|
|
||||||
try {
|
|
||||||
const data = await api.auth.captcha()
|
|
||||||
captchaData.value = data
|
|
||||||
captchaKey.value = data.captcha_key || ''
|
|
||||||
} catch { ElMessage.warning('验证码加载失败,请点击刷新') }
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleRegister() {
|
|
||||||
const valid = await formRef.value.validate().catch(() => false)
|
|
||||||
if (!valid) return
|
|
||||||
loading.value = true
|
|
||||||
try {
|
|
||||||
const payload = { ...form, captcha_key: captchaKey.value }
|
|
||||||
delete payload.confirmPassword
|
|
||||||
const res = await api.auth.register(payload)
|
|
||||||
localStorage.setItem('token', res.token)
|
|
||||||
userStore.username = res.user.username
|
|
||||||
userStore.isVip = res.user.is_vip
|
|
||||||
userStore.tokenChecked = true
|
|
||||||
userStore.tokenValid = true
|
|
||||||
ElMessage.success('注册成功')
|
|
||||||
router.push('/works')
|
|
||||||
} catch (err) {
|
|
||||||
console.error('注册失败:', err)
|
|
||||||
loadCaptcha()
|
|
||||||
form.captcha = ''
|
|
||||||
} finally { loading.value = false }
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.auth-page {
|
|
||||||
min-height: calc(100vh - 120px);
|
|
||||||
display: flex; align-items: center; justify-content: center;
|
|
||||||
padding: 40px 20px;
|
|
||||||
}
|
|
||||||
.auth-card {
|
|
||||||
width: 440px; padding: 36px; border-radius: 12px;
|
|
||||||
background: #fff; box-shadow: 0 8px 32px rgba(0,0,0,.08);
|
|
||||||
h2 { text-align: center; margin-bottom: 24px; font-size: 22px; }
|
|
||||||
.captcha-img {
|
|
||||||
width: 120px; height: 40px; border: 1px solid #dcdfe6; border-radius: 4px;
|
|
||||||
cursor: pointer; overflow: hidden; display: flex; align-items: center; justify-content: center;
|
|
||||||
img { width: 100%; height: 100%; object-fit: cover; }
|
|
||||||
.captcha-text { font-family: monospace; font-size: 18px; letter-spacing: 6px; font-weight: bold; color: #333; }
|
|
||||||
.loading-text { font-size: 12px; letter-spacing: normal; color: #999; }
|
|
||||||
}
|
|
||||||
.switch-text { text-align: center; color: #999; font-size: 13px; a { color: #07c160; } }
|
|
||||||
|
|
||||||
/* 密码强度条 */
|
|
||||||
.pwd-strength {
|
|
||||||
display: flex; align-items: center; gap: 8px; margin-top: 6px;
|
|
||||||
.strength-bar {
|
|
||||||
width: 80px; height: 4px; border-radius: 2px; background: #e0e0e0; transition: background .3s;
|
|
||||||
&.weak { background: #f56c6c; }
|
|
||||||
&.medium { background: #e6a23c; }
|
|
||||||
&.strong { background: #67c23a; }
|
|
||||||
}
|
|
||||||
.strength-label { font-size: 12px; }
|
|
||||||
.weak + .strength-label { color: #f56c6c; }
|
|
||||||
.medium + .strength-label { color: #e6a23c; }
|
|
||||||
.strong + .strength-label { color: #67c23a; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,139 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="works-page">
|
|
||||||
<div class="container">
|
|
||||||
<h2>我的作品</h2>
|
|
||||||
<div class="toolbar">
|
|
||||||
<el-input v-model="keyword" placeholder="搜索作品标题..." prefix-icon="Search" clearable style="width:260px" />
|
|
||||||
<el-radio-group v-model="filterType" size="small">
|
|
||||||
<el-radio-button value="">全部</el-radio-button>
|
|
||||||
<el-radio-button value="chat">聊天</el-radio-button>
|
|
||||||
<el-radio-button value="balance">余额</el-radio-button>
|
|
||||||
</el-radio-group>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="work-list" v-loading="loading">
|
|
||||||
<div v-for="work in worksList" :key="work.id" class="work-card">
|
|
||||||
<div class="card-thumb" :class="work.type">
|
|
||||||
<span>{{ work.type === 'chat' ? '💬' : '💰' }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="card-info">
|
|
||||||
<h4>{{ work.title }}</h4>
|
|
||||||
<p class="meta">{{ work.type === 'chat' ? '聊天记录' : '余额截图' }} · {{ formatDate(work.updated_at) }}</p>
|
|
||||||
</div>
|
|
||||||
<div class="card-actions">
|
|
||||||
<el-button size="small" @click="renameWork(work)">改名</el-button>
|
|
||||||
<el-button size="small" type="primary" plain @click="editWork(work)">编辑</el-button>
|
|
||||||
<el-button size="small" type="danger" plain @click="deleteWork(work)">删除</el-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<el-empty v-if="!loading && !worksList.length" description="暂无作品" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="pagination-wrap" v-if="total > 0">
|
|
||||||
<el-pagination
|
|
||||||
background
|
|
||||||
layout="total, prev, pager, next"
|
|
||||||
:total="total"
|
|
||||||
:page-size="10"
|
|
||||||
:current-page="currentPage"
|
|
||||||
@current-change="onPageChange"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref, onMounted, watch } from 'vue'
|
|
||||||
import { useRouter } from 'vue-router'
|
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
|
||||||
import api from '@/api'
|
|
||||||
|
|
||||||
const router = useRouter()
|
|
||||||
const loading = ref(false)
|
|
||||||
const keyword = ref('')
|
|
||||||
const filterType = ref('')
|
|
||||||
const worksList = ref([])
|
|
||||||
const currentPage = ref(1)
|
|
||||||
const total = ref(0)
|
|
||||||
|
|
||||||
onMounted(loadWorks)
|
|
||||||
|
|
||||||
watch([keyword, filterType], () => { currentPage.value = 1; loadWorks() })
|
|
||||||
|
|
||||||
async function loadWorks() {
|
|
||||||
loading.value = true
|
|
||||||
try {
|
|
||||||
const res = await api.works.list({
|
|
||||||
keyword: keyword.value,
|
|
||||||
type: filterType.value || undefined,
|
|
||||||
page: currentPage.value,
|
|
||||||
per_page: 10,
|
|
||||||
})
|
|
||||||
worksList.value = res.items
|
|
||||||
total.value = res.total
|
|
||||||
} finally { loading.value = false }
|
|
||||||
}
|
|
||||||
|
|
||||||
function onPageChange(page) {
|
|
||||||
currentPage.value = page
|
|
||||||
loadWorks()
|
|
||||||
}
|
|
||||||
|
|
||||||
function editWork(work) {
|
|
||||||
if (work.type === 'chat') router.push(`/chat?workId=${work.id}`)
|
|
||||||
else router.push(`/balance?workId=${work.id}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
async function deleteWork(work) {
|
|
||||||
try { await ElMessageBox.confirm(`确定删除「${work.title}」?`, '确认', { confirmButtonText: '删除', cancelButtonText: '取消', type: 'warning' }) }
|
|
||||||
catch { return }
|
|
||||||
await api.works.delete(work.id)
|
|
||||||
ElMessage.success('已删除')
|
|
||||||
loadWorks()
|
|
||||||
}
|
|
||||||
|
|
||||||
async function renameWork(work) {
|
|
||||||
try {
|
|
||||||
const { value: newTitle } = await ElMessageBox.prompt('请输入新名称', '修改名称', {
|
|
||||||
confirmButtonText: '确定',
|
|
||||||
cancelButtonText: '取消',
|
|
||||||
inputValue: work.title,
|
|
||||||
inputPlaceholder: '输入新作品名称',
|
|
||||||
})
|
|
||||||
if (!newTitle || newTitle === work.title) return
|
|
||||||
await api.works.update(work.id, { title: newTitle })
|
|
||||||
ElMessage.success('已修改')
|
|
||||||
loadWorks()
|
|
||||||
} catch {}
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatDate(iso) { return iso?.slice(0, 16).replace('T', ' ') || '' }
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.works-page { min-height: calc(100vh - 120px); padding: 30px 20px; }
|
|
||||||
.container { max-width: 900px; margin: 0 auto; h2 { margin-bottom: 20px; } }
|
|
||||||
.toolbar { display: flex; align-items: center; gap: 16px; margin-bottom: 20px; }
|
|
||||||
|
|
||||||
.work-list { display: flex; flex-direction: column; gap: 10px; }
|
|
||||||
.work-card {
|
|
||||||
display: flex; align-items: center; gap: 16px;
|
|
||||||
padding: 16px 20px; background: #fff; border-radius: 10px;
|
|
||||||
box-shadow: 0 1px 6px rgba(0,0,0,.04); transition: box-shadow .2s;
|
|
||||||
&:hover { box-shadow: 0 3px 12px rgba(0,0,0,.08); }
|
|
||||||
|
|
||||||
.card-thumb {
|
|
||||||
width: 48px; height: 48px; border-radius: 10px;
|
|
||||||
display: flex; align-items: center; justify-content: center; font-size: 22px;
|
|
||||||
&.chat { background: #e8f5e9; }
|
|
||||||
&.balance { background: #fff8e1; }
|
|
||||||
}
|
|
||||||
.card-info { flex: 1; h4 { font-size: 15px; margin-bottom: 2px; } .meta { font-size: 12px; color: #999; } }
|
|
||||||
.card-actions { display: flex; gap: 6px; }
|
|
||||||
}
|
|
||||||
.pagination-wrap {
|
|
||||||
display: flex; justify-content: center;
|
|
||||||
padding: 20px 0 10px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -0,0 +1,653 @@
|
||||||
|
<template>
|
||||||
|
<div class="article-detail-wrapper">
|
||||||
|
<div class="main-container">
|
||||||
|
|
||||||
|
<!-- 左侧核心内容区 -->
|
||||||
|
<div class="article-left-content">
|
||||||
|
<!-- 顶部返回按钮 -->
|
||||||
|
<div class="back-btn-box">
|
||||||
|
<div class="back-btn" @click="handleBack">返回列表</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 主文章卡片 -->
|
||||||
|
<div class="article-main-card">
|
||||||
|
<!-- 标题 -->
|
||||||
|
<h1 class="article-title">{{ currentArticle.title }}</h1>
|
||||||
|
|
||||||
|
<!-- 发布日期与浏览量 -->
|
||||||
|
<div class="article-meta">
|
||||||
|
<div class="meta-item">
|
||||||
|
<img class="meta-icon" src="@/public/article/date.png" alt="日期">
|
||||||
|
<span>{{ currentArticle.date }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="meta-item">
|
||||||
|
<img class="meta-icon" src="@/public/article/eye.png" alt="浏览数">
|
||||||
|
<span>{{ currentArticle.views }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 文章导读区 -->
|
||||||
|
<div class="article-intro-box">
|
||||||
|
<div class="intro-header">
|
||||||
|
<img class="star-icon" src="@/public/article/start.png" />
|
||||||
|
<span>文章导读</span>
|
||||||
|
</div>
|
||||||
|
<p class="intro-content">{{ currentArticle.intro || currentArticle.description }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 正文区块 -->
|
||||||
|
<div class="article-body-content">
|
||||||
|
<template v-if="currentArticle.sections && currentArticle.sections.length">
|
||||||
|
<div v-for="(sec, idx) in currentArticle.sections" :key="idx" class="section-block">
|
||||||
|
<h3 class="section-title">{{ sec.heading }}</h3>
|
||||||
|
<p class="section-text">{{ sec.content }}</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<div class="section-block">
|
||||||
|
<h3 class="section-title">一、功能介绍</h3>
|
||||||
|
<p class="section-text">
|
||||||
|
我们不做“假图”,我们做的是“比真图更真的模拟器”。无论是你需要一张转账截图、一套朋友圈互动记录,还是一整份零钱账单流水,这款模拟器都能帮你瞬间完成。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 文章插图 -->
|
||||||
|
<div class="article-image-box">
|
||||||
|
<img v-if="currentArticle.image" :src="currentArticle.image" alt="文章插图"
|
||||||
|
class="article-img" />
|
||||||
|
<img v-else src="https://picsum.photos/800/320?random=1" alt="演示图" class="article-img" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 上一篇 / 下一篇 导航卡片 -->
|
||||||
|
<div class="article-nav-section">
|
||||||
|
<div class="nav-card prev-card" @click="handleSwitchArticle(prevArticle)">
|
||||||
|
<div class="nav-arrow left-arrow">
|
||||||
|
<img style="width:100%;height:100%;" src="@/public/article/left.png" alt="">
|
||||||
|
</div>
|
||||||
|
<div class="nav-info">
|
||||||
|
<span class="nav-label">上一篇</span>
|
||||||
|
<span class="nav-title" :title="prevArticle?.title">{{ prevArticle?.title || '没有上一篇了'
|
||||||
|
}}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="nav-card next-card" @click="handleSwitchArticle(nextArticle)">
|
||||||
|
<div class="nav-info align-right">
|
||||||
|
<span class="nav-label">下一篇</span>
|
||||||
|
<span class="nav-title" :title="nextArticle?.title">{{ nextArticle?.title || '没有下一篇了'
|
||||||
|
}}</span>
|
||||||
|
</div>
|
||||||
|
<div class="nav-arrow right-arrow">
|
||||||
|
<img style="width:100%;height:100%;" src="@/public/article/right.png" alt="">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 右侧相关推荐侧边栏 -->
|
||||||
|
<div class="recommend-sidebar">
|
||||||
|
<div class="sidebar-card">
|
||||||
|
<div class="sidebar-header">
|
||||||
|
<img src="@/public/article/tuijian.png" class="recommend-icon" />
|
||||||
|
<span class="sidebar-title">相关推荐</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="recommend-list">
|
||||||
|
<div v-for="(rec, index) in recommendList" :key="rec.id" class="recommend-item"
|
||||||
|
@click="handleSwitchArticle(rec)">
|
||||||
|
<div class="rec-rank" :class="{ 'top-rank': index < 3, 'four-rank': index == 3 }">{{ index +
|
||||||
|
1
|
||||||
|
}}</div>
|
||||||
|
<div class="rec-content">
|
||||||
|
<h4 class="rec-title" :title="rec.title">{{ rec.title }}</h4>
|
||||||
|
<div class="rec-meta">
|
||||||
|
<div class="meta-sub">
|
||||||
|
<img class="meta-sub-icon" src="@/public/article/date.png" alt="日期">
|
||||||
|
<span>{{ rec.date }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="meta-sub">
|
||||||
|
<img class="meta-sub-icon" src="@/public/article/eye.png" alt="浏览">
|
||||||
|
<span>{{ rec.views }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 底部警告组件 -->
|
||||||
|
<WarningFooter />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed, watch } from 'vue'
|
||||||
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
|
import WarningFooter from '@/components/common/WarningFooter.vue'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
|
// 当前文章 ID
|
||||||
|
const currentId = ref(Number(route.query.id) || 1)
|
||||||
|
|
||||||
|
watch(() => route.query.id, (newId) => {
|
||||||
|
if (newId) {
|
||||||
|
currentId.value = Number(newId)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Mock 完整文章数据库
|
||||||
|
const articleDatabase = ref([
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: '微商模拟神器-节假日活动必备玩法',
|
||||||
|
date: '2026-7-20',
|
||||||
|
views: '11.5K',
|
||||||
|
intro: '我们不做“假图”,我们做的是“比真图更真的模拟器”。无论是你需要一张转账截图、一套朋友圈互动记录,还是一整份零钱账单流水,这款模拟器都能帮你瞬间完成。',
|
||||||
|
sections: [
|
||||||
|
{
|
||||||
|
heading: '一、功能介绍',
|
||||||
|
content: '我们不做“假图”,我们做的是“比真图更真的模拟器”。无论是你需要一张转账截图、一套朋友圈互动记录,还是一整份零钱账单流水,这款模拟器都能帮你瞬间完成。'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
image: 'https://picsum.photos/800/320?random=10'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
title: '微商模拟神器-节假日活动必备玩法超超超推荐',
|
||||||
|
date: '2026-7-20',
|
||||||
|
views: '11.5K',
|
||||||
|
intro: '全网首创的高逼真恶搞模拟工具,支持自定义头像、对话内容、账单金额与交易状态,各种场景真实不穿帮。',
|
||||||
|
sections: [
|
||||||
|
{
|
||||||
|
heading: '一、核心亮点',
|
||||||
|
content: '一键生成高清对话截图,像素级细节还原,支持无限修改,帮助你轻松制作搞笑段子与社交互动图。'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
image: 'https://picsum.photos/800/320?random=11'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
title: '微商模拟神器-节假日活动必备玩法超超超推荐HHHHH...',
|
||||||
|
date: '2026-7-20',
|
||||||
|
views: '11.5K',
|
||||||
|
intro: '无论是发朋友圈互动还是朋友间轻松恶搞,装样大师都能提供全套的模拟支持。',
|
||||||
|
sections: [
|
||||||
|
{
|
||||||
|
heading: '一、快速上手',
|
||||||
|
content: '无需复杂的操作步骤,选择模板填入文字即可一键导出无水印图片。'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
image: 'https://picsum.photos/800/320?random=12'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
title: '微信神器转账支付怎么制作?装样大师新手教程',
|
||||||
|
date: '2026-7-20',
|
||||||
|
views: '11.5K',
|
||||||
|
intro: '新手必看!如何三步快速生成高清逼真的转账与零钱余额截图。',
|
||||||
|
sections: [
|
||||||
|
{
|
||||||
|
heading: '一、教程步骤',
|
||||||
|
content: '打开微信模拟功能,输入期望金额与收付款人信息,点击保存按钮导出图片。'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
image: 'https://picsum.photos/800/320?random=13'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
title: '工资单与机票模拟生成器全面上线说明',
|
||||||
|
date: '2026-7-20',
|
||||||
|
views: '11.5K',
|
||||||
|
intro: '全新升级娱乐模拟玩法,支持模拟机票、高铁票及个人工资单记录。',
|
||||||
|
sections: [
|
||||||
|
{
|
||||||
|
heading: '一、更新公告',
|
||||||
|
content: '优化底纹防伪效果,提升了图片生成的画质与加载速度。'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
image: 'https://picsum.photos/800/320?random=14'
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
// 匹配当前 ID 对应的文章
|
||||||
|
const currentArticle = computed(() => {
|
||||||
|
return articleDatabase.value.find(item => item.id === currentId.value) || articleDatabase.value[0]
|
||||||
|
})
|
||||||
|
|
||||||
|
// 计算上一篇与下一篇
|
||||||
|
const currentIndex = computed(() => {
|
||||||
|
return articleDatabase.value.findIndex(item => item.id === currentArticle.value.id)
|
||||||
|
})
|
||||||
|
|
||||||
|
const prevArticle = computed(() => {
|
||||||
|
if (currentIndex.value > 0) {
|
||||||
|
return articleDatabase.value[currentIndex.value - 1]
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
|
||||||
|
const nextArticle = computed(() => {
|
||||||
|
if (currentIndex.value < articleDatabase.value.length - 1) {
|
||||||
|
return articleDatabase.value[currentIndex.value + 1]
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
|
||||||
|
// 相关推荐列表(包含 5 条数据)
|
||||||
|
const recommendList = computed(() => articleDatabase.value)
|
||||||
|
|
||||||
|
function handleBack() {
|
||||||
|
router.push('/article')
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSwitchArticle(articleItem) {
|
||||||
|
if (!articleItem) return
|
||||||
|
router.push({
|
||||||
|
path: '/article-detail',
|
||||||
|
query: { id: articleItem.id }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.article-detail-wrapper {
|
||||||
|
position: relative;
|
||||||
|
min-height: calc(100vh - 112px);
|
||||||
|
background-color: #F8F9FB;
|
||||||
|
padding: 36px 20px 80px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-container {
|
||||||
|
width: 800px;
|
||||||
|
position: relative;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 左侧主体内容:相对于整个页面水平居中 */
|
||||||
|
.article-left-content {
|
||||||
|
width: 800px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 右侧侧边栏:固定在居中主卡片的右侧,且不跟随页面滚动 */
|
||||||
|
.recommend-sidebar {
|
||||||
|
width: 310px;
|
||||||
|
position: fixed;
|
||||||
|
top: 201px;
|
||||||
|
left: calc(50% + 424px);
|
||||||
|
/* 正好定位在 800px 居中卡片右侧 24px 处 */
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1480px) {
|
||||||
|
.recommend-sidebar {
|
||||||
|
left: auto;
|
||||||
|
right: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1180px) {
|
||||||
|
.recommend-sidebar {
|
||||||
|
display: none;
|
||||||
|
/* 屏幕过窄时隐藏侧边栏避免挡住主卡片 */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 返回按钮 */
|
||||||
|
.back-btn-box {
|
||||||
|
margin-bottom: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-btn {
|
||||||
|
width: 116px;
|
||||||
|
height: 36px;
|
||||||
|
background-image: url('/src/public/article/back-bg.png');
|
||||||
|
background-size: 100% 100%;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
text-align: right;
|
||||||
|
padding-right: 18px;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 16px;
|
||||||
|
color: #FFFFFF;
|
||||||
|
line-height: 36px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 0.92;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 主卡片 */
|
||||||
|
.article-main-card {
|
||||||
|
background: #FFFFFF;
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 34px 30px 20px;
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.04);
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-title {
|
||||||
|
margin: 0 0 18px 0;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 18px;
|
||||||
|
color: #1A1A1A;
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-meta {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 20px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
|
||||||
|
.meta-item {
|
||||||
|
height: 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
font-size: 18px;
|
||||||
|
color: #AAAAAA;
|
||||||
|
line-height: 18px;
|
||||||
|
text-align: left;
|
||||||
|
|
||||||
|
.meta-icon {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 导读区 */
|
||||||
|
.article-intro-box {
|
||||||
|
background-color: #F8F6FF;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 16px 16px 12px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
|
||||||
|
.intro-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 16px;
|
||||||
|
color: #965DF8;
|
||||||
|
line-height: 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
|
||||||
|
.star-icon {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
margin-right: 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.intro-content {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 16px;
|
||||||
|
color: #767676;
|
||||||
|
line-height: 26px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 正文 */
|
||||||
|
.article-body-content {
|
||||||
|
.section-block {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 26px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1A1A1A;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-text {
|
||||||
|
font-size: 16px;
|
||||||
|
color: #1A1A1A;
|
||||||
|
line-height: 26px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-image-box {
|
||||||
|
width: 100%;
|
||||||
|
height: 320px;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
|
||||||
|
.article-img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 上一篇/下一篇 导航卡片 */
|
||||||
|
.article-nav-section {
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
margin-top: 32px;
|
||||||
|
|
||||||
|
.nav-card {
|
||||||
|
flex: 1;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
box-sizing: border-box;
|
||||||
|
background: #FFFFFF;
|
||||||
|
border: 1px solid #EAEAEA;
|
||||||
|
|
||||||
|
&.prev-card {
|
||||||
|
&:hover {
|
||||||
|
background: #F6F3FD;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.next-card {
|
||||||
|
&:hover {
|
||||||
|
background: #F0EBFF;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-arrow {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(139, 112, 255, 0.12);
|
||||||
|
color: #8B70FF;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
padding: 0 16px;
|
||||||
|
|
||||||
|
&.align-right {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-label {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1A1A1A;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-title {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #767676;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 右侧侧边栏 */
|
||||||
|
.recommend-sidebar {
|
||||||
|
width: 230px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-card {
|
||||||
|
background: #FFFFFF;
|
||||||
|
border-radius: 18px;
|
||||||
|
padding: 0;
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.04);
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-header {
|
||||||
|
position: relative;
|
||||||
|
height: 46px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 0 12px;
|
||||||
|
color: #8B70FF;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
height: 1px;
|
||||||
|
background-color: #D8D8D8;
|
||||||
|
transform: scaleY(0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.recommend-icon {
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-title {
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 16px;
|
||||||
|
color: #1A1A1A;
|
||||||
|
line-height: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.recommend-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recommend-item {
|
||||||
|
height: 86px;
|
||||||
|
padding-left: 18px;
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
padding-bottom: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.rec-title {
|
||||||
|
color: #8B70FF;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.rec-rank {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #D8D8D8;
|
||||||
|
width: 18px;
|
||||||
|
|
||||||
|
&.top-rank {
|
||||||
|
color: #965DF8;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.four-rank {
|
||||||
|
color: #AAAAAA;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.rec-content {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
height: 100%;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
border-bottom: 1px solid #F0F0F0;
|
||||||
|
padding-right: 10px;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rec-title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #1A1A1A;
|
||||||
|
line-height: 1.4;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
transition: color 0.2s;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rec-meta {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
|
||||||
|
.meta-sub {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #AAAAAA;
|
||||||
|
|
||||||
|
.meta-sub-icon {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 960px) {
|
||||||
|
.main-container {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recommend-sidebar {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,310 @@
|
||||||
|
<template>
|
||||||
|
<div class="article-page-wrapper">
|
||||||
|
<div class="article-list-container">
|
||||||
|
<div v-for="item in articleList" :key="item.id" class="article-card" @click="handleCardClick(item)">
|
||||||
|
<!-- 卡片头部信息 -->
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="header-left">
|
||||||
|
<h2 class="article-title" :title="item.title">{{ item.title }}</h2>
|
||||||
|
<div v-if="item.isHot" class="hot-badge">
|
||||||
|
<img class="hot-icon" src="@/public/article/hot.png" alt="">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="header-meta">
|
||||||
|
<div class="meta-item">
|
||||||
|
<img class="meta-icon" src="@/public/article/date.png" alt="">
|
||||||
|
<span>{{ item.date }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="meta-item">
|
||||||
|
<img class="meta-icon" src="@/public/article/eye.png" alt="">
|
||||||
|
<span>{{ item.views }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 卡片主体内容 -->
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="cover-thumb">
|
||||||
|
<img v-if="item.cover" :src="item.cover" :alt="item.title" class="cover-img" />
|
||||||
|
<div v-else class="cover-placeholder"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="content-right">
|
||||||
|
<p class="article-desc">{{ item.description }}</p>
|
||||||
|
<div class="action-box">
|
||||||
|
<button class="detail-btn" @click.stop="handleDetail(item)">查看详情</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 底部警告说明组件 -->
|
||||||
|
<WarningFooter />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import WarningFooter from '@/components/common/WarningFooter.vue'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
// 依照效果图高保真模拟数据(支持接口数据列表直接替换)
|
||||||
|
const articleList = ref([
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: '微商模拟神器',
|
||||||
|
isHot: true,
|
||||||
|
date: '2026-7-20',
|
||||||
|
views: '11.5K',
|
||||||
|
cover: '',
|
||||||
|
description: '我们不做“假图”,我们做的是“比真图更真的模拟器”。无论是你需要一张转账截图、一套朋友圈互动记录,还是一整份零钱账单流水,这款模拟器都能帮你瞬间完成。我们不做“假图”,我们做的是“比真图更真的模拟器”。无论是你需要一张转账截图、一套朋友圈互动记录,还是一整份零钱账单流水,这款模拟器都能帮你瞬间完成。'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
title: '微商模拟神器哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈好...',
|
||||||
|
isHot: true,
|
||||||
|
date: '2026-7-20',
|
||||||
|
views: '11.5K',
|
||||||
|
cover: '',
|
||||||
|
description: '我们不做“假图”,我们做的是“比真图更真的模拟器”。无论是你需要一张转账截图、一套朋友圈互动记录,还是一整份零钱账单流水,这款模拟器都能帮你瞬间完成。哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈...'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: '微商模拟神器',
|
||||||
|
isHot: true,
|
||||||
|
date: '2026-7-20',
|
||||||
|
views: '11.5K',
|
||||||
|
cover: '',
|
||||||
|
description: '我们不做“假图”,我们做的是“比真图更真的模拟器”。无论是你需要一张转账截图、一套朋友圈互动记录,还是一整份零钱账单流水,这款模拟器都能帮你瞬间完成。'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
title: '微商模拟神器哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈好哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈',
|
||||||
|
isHot: true,
|
||||||
|
date: '2026-7-20',
|
||||||
|
views: '11.5K',
|
||||||
|
cover: '',
|
||||||
|
description: '我们不做“假图”,我们做的是“比真图更真的模拟器”。无论是你需要一张转账截图、一套朋友圈互动记录,还是一整份零钱账单流水,这款模拟器都能帮你瞬间完成。哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈...'
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
function handleDetail(item) {
|
||||||
|
router.push({
|
||||||
|
path: '/article-detail',
|
||||||
|
query: { id: item.id }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCardClick(item) {
|
||||||
|
handleDetail(item)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.article-page-wrapper {
|
||||||
|
min-height: calc(100vh - 112px);
|
||||||
|
background-color: #F8F9FB;
|
||||||
|
padding: 40px 20px 80px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-list-container {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 820px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-card {
|
||||||
|
width: 800px;
|
||||||
|
background: #FFFFFF;
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 34px;
|
||||||
|
cursor: pointer;
|
||||||
|
box-sizing: border-box;
|
||||||
|
box-shadow: 0px 0px 12px 0px rgba(0, 0, 0, 0.1);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
box-shadow: 0 8px 15px rgba(0, 0, 0, 0.07);
|
||||||
|
transform: scale(1.005);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 卡片头部样式 */
|
||||||
|
.card-header {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding-bottom: 20px;
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 1px;
|
||||||
|
background-color: #D8D8D8;
|
||||||
|
transform: scaleY(0.5);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
/* 允许标题自适应缩减触发 ellipsis */
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-title {
|
||||||
|
max-width: 468px;
|
||||||
|
margin: 0;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 18px;
|
||||||
|
color: #1A1A1A;
|
||||||
|
line-height: 20px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* HOT 标签样式 */
|
||||||
|
.hot-badge {
|
||||||
|
width: 41px;
|
||||||
|
height: 18px;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-meta {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
color: #999999;
|
||||||
|
font-size: 14px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
font-size: 18px;
|
||||||
|
color: #AAAAAA;
|
||||||
|
line-height: 18px;
|
||||||
|
|
||||||
|
.meta-icon {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 卡片主体内容布局 */
|
||||||
|
.card-body {
|
||||||
|
display: flex;
|
||||||
|
gap: 28px;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cover-thumb {
|
||||||
|
width: 170px;
|
||||||
|
height: 125px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
border-radius: 6px;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.cover-img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cover-placeholder {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: #D8D8D8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-right {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
flex: 1;
|
||||||
|
min-height: 125px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-desc {
|
||||||
|
margin: 0;
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 16px;
|
||||||
|
color: #767676;
|
||||||
|
line-height: 26px;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 3;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-box {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-btn {
|
||||||
|
width: 100px;
|
||||||
|
height: 36px;
|
||||||
|
background: linear-gradient(215deg, #A679F4 7.14%, #778CF5 100%);
|
||||||
|
color: #FFFFFF;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
box-shadow: 0 4px 12px rgba(120, 84, 255, 0.25);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 0.92;
|
||||||
|
box-shadow: 0 6px 16px rgba(120, 84, 255, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.card-body {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cover-thumb {
|
||||||
|
width: 100%;
|
||||||
|
height: 160px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-right {
|
||||||
|
min-height: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -55,7 +55,7 @@
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import AppFooter from '@/components/AppFooter.vue'
|
import AppFooter from '@/components/common/AppFooter.vue'
|
||||||
import ToolCard from '@/components/ToolCard.vue'
|
import ToolCard from '@/components/ToolCard.vue'
|
||||||
import { useRouter, useRoute } from 'vue-router'
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
|
||||||
|
|
@ -103,7 +103,7 @@
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, watch } from 'vue'
|
import { ref, computed, watch } from 'vue'
|
||||||
import WarningFooter from '@/components/WarningFooter.vue'
|
import WarningFooter from '@/components/common/WarningFooter.vue'
|
||||||
import { all } from 'axios'
|
import { all } from 'axios'
|
||||||
import { dayOrDaysToDate, ElMessage } from 'element-plus'
|
import { dayOrDaysToDate, ElMessage } from 'element-plus'
|
||||||
import html2canvas from 'html2canvas'
|
import html2canvas from 'html2canvas'
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,7 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { Search, Close, EditPen, Download } from '@element-plus/icons-vue'
|
import { Search, Close, EditPen, Download } from '@element-plus/icons-vue'
|
||||||
import WarningFooter from '@/components/WarningFooter.vue'
|
import WarningFooter from '@/components/common/WarningFooter.vue'
|
||||||
|
|
||||||
const searchQuery = ref('')
|
const searchQuery = ref('')
|
||||||
const tabs = ['全部', '微信模拟', '娱乐模拟']
|
const tabs = ['全部', '微信模拟', '娱乐模拟']
|
||||||
|
|
|
||||||
|
|
@ -112,6 +112,7 @@ import SimulatorLayout from '@/components/simulator/SimulatorLayout.vue'
|
||||||
import api from '@/api'
|
import api from '@/api'
|
||||||
import { useUserStore } from '@/stores/user'
|
import { useUserStore } from '@/stores/user'
|
||||||
import html2canvas from 'html2canvas'
|
import html2canvas from 'html2canvas'
|
||||||
|
import { checkUserVip } from '@/utils/appUtils'
|
||||||
|
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
const showPayment = inject('showPayment')
|
const showPayment = inject('showPayment')
|
||||||
|
|
@ -122,14 +123,6 @@ const layoutRef = ref()
|
||||||
const previewAreaRef = computed(() => layoutRef.value?.previewAreaRef)
|
const previewAreaRef = computed(() => layoutRef.value?.previewAreaRef)
|
||||||
|
|
||||||
|
|
||||||
watch(() => userStore.isVip, (isVip) => {
|
|
||||||
if (isVip) {
|
|
||||||
removeWatermark()
|
|
||||||
} else {
|
|
||||||
createWatermark()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const balanceSettings = reactive({
|
const balanceSettings = reactive({
|
||||||
statusTime: new Date().toTimeString().slice(0, 5),
|
statusTime: new Date().toTimeString().slice(0, 5),
|
||||||
phoneModel: 'ios',
|
phoneModel: 'ios',
|
||||||
|
|
@ -157,10 +150,6 @@ const workId = ref(null)
|
||||||
const originalTitle = ref('')
|
const originalTitle = ref('')
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
if (!userStore.isVip) {
|
|
||||||
createWatermark()
|
|
||||||
}
|
|
||||||
|
|
||||||
const id = route.query.workId
|
const id = route.query.workId
|
||||||
if (!id) return
|
if (!id) return
|
||||||
try {
|
try {
|
||||||
|
|
@ -212,13 +201,6 @@ async function handleSave() {
|
||||||
async function handleExport() {
|
async function handleExport() {
|
||||||
if (!localStorage.getItem('token')) { ElMessage.warning('请先登录后再下载'); return }
|
if (!localStorage.getItem('token')) { ElMessage.warning('请先登录后再下载'); return }
|
||||||
if (!userStore.isVip) {
|
if (!userStore.isVip) {
|
||||||
try {
|
|
||||||
await ElMessageBox.confirm(
|
|
||||||
'下载高清截图需要VIP会员权限,是否前往开通?',
|
|
||||||
'会员限制',
|
|
||||||
{ confirmButtonText: '去开通', cancelButtonText: '取消', type: 'warning' }
|
|
||||||
)
|
|
||||||
} catch { return }
|
|
||||||
showPayment?.()
|
showPayment?.()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -427,6 +427,7 @@ import SimulatorLayout from '@/components/simulator/SimulatorLayout.vue'
|
||||||
import api from '@/api'
|
import api from '@/api'
|
||||||
import { useUserStore } from '@/stores/user'
|
import { useUserStore } from '@/stores/user'
|
||||||
import html2canvas from 'html2canvas'
|
import html2canvas from 'html2canvas'
|
||||||
|
import { checkUserVip } from '@/utils/appUtils'
|
||||||
|
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
const showPayment = inject('showPayment')
|
const showPayment = inject('showPayment')
|
||||||
|
|
@ -436,14 +437,6 @@ const layoutRef = ref()
|
||||||
const previewRef = ref()
|
const previewRef = ref()
|
||||||
const previewAreaRef = computed(() => layoutRef.value?.previewAreaRef)
|
const previewAreaRef = computed(() => layoutRef.value?.previewAreaRef)
|
||||||
|
|
||||||
watch(() => userStore.isVip, (isVip) => {
|
|
||||||
if (isVip) {
|
|
||||||
removeWatermark()
|
|
||||||
} else {
|
|
||||||
createWatermark()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const tabs = ref([
|
const tabs = ref([
|
||||||
{ label: '聊天设置', value: 'edit' },
|
{ label: '聊天设置', value: 'edit' },
|
||||||
{ label: '外观设置', value: 'appearance' }
|
{ label: '外观设置', value: 'appearance' }
|
||||||
|
|
@ -538,10 +531,6 @@ function handleSystemQuickAdd(command) {
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
if (!userStore.isVip) {
|
|
||||||
createWatermark()
|
|
||||||
}
|
|
||||||
|
|
||||||
const id = route.query.workId
|
const id = route.query.workId
|
||||||
if (!id) return
|
if (!id) return
|
||||||
try {
|
try {
|
||||||
|
|
@ -760,7 +749,6 @@ async function handleSave() {
|
||||||
async function handleExport() {
|
async function handleExport() {
|
||||||
if (!localStorage.getItem('token')) { ElMessage.warning('请先登录后再下载'); return }
|
if (!localStorage.getItem('token')) { ElMessage.warning('请先登录后再下载'); return }
|
||||||
if (!userStore.isVip) {
|
if (!userStore.isVip) {
|
||||||
try { await ElMessageBox.confirm('下载高清截图需要VIP会员权限,是否前往开通?', '会员限制', { confirmButtonText: '去开通', cancelButtonText: '取消', type: 'warning' }) } catch { return }
|
|
||||||
showPayment?.()
|
showPayment?.()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||