fix: 协议勾选控制+news详情页样式修复+支付页重做

This commit is contained in:
Yuzhiran Dev
2026-07-12 17:43:32 +08:00
parent 23de8bc86a
commit 5ac33dcd7f
5 changed files with 325 additions and 213 deletions
+2 -1
View File
@@ -148,7 +148,8 @@ function onNotify() {
} }
function goNewsDetail(item) { function goNewsDetail(item) {
uni.switchTab({ url: '/pages/trend/trend' }) uni.setStorageSync('__news_item__', JSON.stringify(item))
uni.navigateTo({ url: '/pages/news/news' })
} }
</script> </script>
+8
View File
@@ -133,6 +133,10 @@ const BASE_URL = 'https://wdkj.yuzhiran.com.cn'
/** 发送邮箱验证码 */ /** 发送邮箱验证码 */
async function handleSendCode() { async function handleSendCode() {
if (!agreed.value) {
uni.showToast({ title: '请先阅读并同意用户协议和隐私政策', icon: 'none' })
return
}
const email = registerForm.value.email const email = registerForm.value.email
if (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) { if (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
uni.showToast({ title: '请先输入正确的邮箱', icon: 'none' }) uni.showToast({ title: '请先输入正确的邮箱', icon: 'none' })
@@ -170,6 +174,10 @@ async function handleSendCode() {
} }
const wechatLogin = async () => { const wechatLogin = async () => {
if (!loginAgreed.value) {
uni.showToast({ title: '请先阅读并同意用户协议和隐私政策', icon: 'none' })
return
}
try { try {
const loginRes = await new Promise((resolve, reject) => { const loginRes = await new Promise((resolve, reject) => {
uni.login({ success: resolve, fail: reject }) uni.login({ success: resolve, fail: reject })
+15 -14
View File
@@ -198,7 +198,7 @@ onMounted(() => {
.nav-back { .nav-back {
font-size: 28px; font-size: 28px;
color: #fff; color: var(--text-primary);
padding: 0 8px; padding: 0 8px;
line-height: 1; line-height: 1;
} }
@@ -209,7 +209,7 @@ onMounted(() => {
color: var(--text-primary); color: var(--text-primary);
} }
.nav-share { font-size: 22px; color: rgba(255,255,255,0.5); } .nav-share { font-size: 22px; color: var(--text-muted); }
.content { .content {
flex: 1; flex: 1;
@@ -218,18 +218,19 @@ onMounted(() => {
// Hero // Hero
.news-hero { .news-hero {
height: 180px; height: 160px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
border-radius: 0 0 24px 24px; border-radius: 0 0 24px 24px;
margin: 0 20px;
} }
.hero-emoji { font-size: 72px; } .hero-emoji { font-size: 64px; }
// Category // Category
.category-tag { .category-tag {
margin: 24px 20px 0; margin: 20px 20px 0;
} }
.tag-text { .tag-text {
@@ -270,19 +271,19 @@ onMounted(() => {
.meta-label { .meta-label {
font-size: 11px; font-size: 11px;
color: rgba(255,255,255,0.35); color: var(--text-tertiary);
} }
.meta-label.hot { font-size: 13px; } .meta-label.hot { font-size: 13px; }
.meta-value { font-size: 12px; color: rgba(255,255,255,0.7); } .meta-value { font-size: 12px; color: var(--text-secondary); }
// Summary // Summary
.summary-card { .summary-card {
margin: 0 20px 20px; margin: 0 20px 20px;
padding: 16px; padding: 16px;
background: linear-gradient(135deg, rgba(179,136,255,0.08), rgba(77,208,225,0.05)); background: linear-gradient(135deg, rgba(99,102,241,0.06), rgba(0,188,212,0.04));
border: 1px solid rgba(179,136,255,0.12); border: 1px solid rgba(99,102,241,0.1);
border-radius: 12px; border-radius: 12px;
} }
@@ -297,7 +298,7 @@ onMounted(() => {
.summary-text { .summary-text {
font-size: 14px; font-size: 14px;
line-height: 1.7; line-height: 1.7;
color: rgba(255,255,255,0.85); color: var(--text-primary);
} }
// Body // Body
@@ -305,7 +306,7 @@ onMounted(() => {
margin: 0 20px 20px; margin: 0 20px 20px;
font-size: 14px; font-size: 14px;
line-height: 1.8; line-height: 1.8;
color: rgba(255,255,255,0.7); color: var(--text-secondary);
} }
// Related tags // Related tags
@@ -316,7 +317,7 @@ onMounted(() => {
.related-label { .related-label {
display: block; display: block;
font-size: 12px; font-size: 12px;
color: rgba(255,255,255,0.4); color: var(--text-tertiary);
margin-bottom: 10px; margin-bottom: 10px;
} }
@@ -329,7 +330,7 @@ onMounted(() => {
border-radius: 20px; border-radius: 20px;
} }
.tag-text { font-size: 12px; color: rgba(255,255,255,0.6); } .tag-item .tag-text { font-size: 12px; color: var(--text-secondary); }
// Action bar // Action bar
.action-bar { .action-bar {
@@ -351,7 +352,7 @@ onMounted(() => {
} }
.action-icon { font-size: 18px; } .action-icon { font-size: 18px; }
.action-text { font-size: 13px; color: rgba(255,255,255,0.7); } .action-text { font-size: 13px; color: var(--text-secondary); }
.footer-spacer { height: 60px; } .footer-spacer { height: 60px; }
</style> </style>
+297 -195
View File
@@ -9,72 +9,111 @@
<view class="nav-placeholder"></view> <view class="nav-placeholder"></view>
</view> </view>
<!-- 头部 --> <scroll-view scroll-y class="scroll-area">
<view class="payment-header"> <!-- 头部 -->
<text class="header-title">解锁 AI 维度 Pro</text> <view class="payment-header">
<text class="header-desc">无限 AI 问答 · 全量知识解锁 · 每日趋势优先推送</text> <view class="header-icon-wrap">
</view> <text class="header-icon"></text>
</view>
<!-- 方案选择 --> <text class="header-title">解锁 AI 维度 Pro</text>
<view class="plan-selector"> <text class="header-desc">无限 AI 问答 · 全量知识 · 趋势优先推送</text>
<view
class="plan-tag"
:class="{ active: selectedPlan === 'pro' }"
@click="selectedPlan = 'pro'"
>
<text class="plan-name">Pro</text>
<text class="plan-price">¥19.9<span class="unit">/</span></text>
<text class="plan-badge">推荐</text>
</view> </view>
<view
class="plan-tag vip"
:class="{ active: selectedPlan === 'vip' }"
@click="selectedPlan = 'vip'"
>
<text class="plan-name">VIP</text>
<text class="plan-price">¥39.9<span class="unit">/</span></text>
<text class="plan-desc"> Pro 全部权益</text>
</view>
</view>
<!-- 权益对比 --> <!-- 方案选择卡片 -->
<view class="benefits"> <view class="plan-selector">
<view class="benefit-item" v-for="(item, idx) in benefits" :key="idx">
<text class="b-icon">{{ item.icon }}</text>
<text class="b-text">{{ item.text }}</text>
</view>
</view>
<!-- 套餐选择 -->
<view class="duration-section">
<text class="duration-title">选择套餐</text>
<view class="duration-list">
<view <view
class="duration-item" class="plan-card"
:class="{ active: selectedDuration === item.key }" :class="{ active: selectedPlan === 'pro' }"
v-for="item in durations" @click="selectedPlan = 'pro'"
:key="item.key"
@click="selectedDuration = item.key"
> >
<text class="d-label">{{ item.label }}</text> <view class="plan-top">
<text class="d-price">¥{{ item.price }}</text> <text class="plan-name">Pro</text>
<text class="d-save" v-if="item.save"> {{ item.save }}</text> <view class="plan-badge" v-if="selectedPlan === 'pro'">推荐</view>
</view>
<text class="plan-price">¥19.9</text>
<text class="plan-unit">/ </text>
<view class="plan-features">
<text class="plan-feature"> AI 问答无限次</text>
<text class="plan-feature"> 全量知识解锁</text>
<text class="plan-feature"> 收藏无限容量</text>
</view>
</view>
<view
class="plan-card vip"
:class="{ active: selectedPlan === 'vip' }"
@click="selectedPlan = 'vip'"
>
<view class="plan-top">
<text class="plan-name">VIP</text>
<view class="plan-badge vip-badge" v-if="selectedPlan === 'vip'">尊享</view>
</view>
<text class="plan-price vip-price">¥39.9</text>
<text class="plan-unit">/ </text>
<view class="plan-features">
<text class="plan-feature"> Pro 全部权益</text>
<text class="plan-feature"> 专属客服支持</text>
<text class="plan-feature"> 专属会员标识</text>
</view>
</view> </view>
</view> </view>
</view>
<!-- 支付按钮 --> <!-- 套餐时长 -->
<view class="pay-section"> <view class="duration-section">
<button class="pay-btn" :loading="loading" @click="handlePay"> <text class="section-title">选择套餐</text>
立即支付 ¥{{ currentPrice }} <view class="duration-list">
</button> <view
<text class="pay-agreement"> class="duration-item"
点击支付即同意用户协议隐私政策 :class="{ active: selectedDuration === item.key }"
</text> v-for="item in durationOptions"
</view> :key="item.key"
@click="selectedDuration = item.key"
>
<text class="d-label">{{ item.label }}</text>
<text class="d-price">¥{{ item.price }}</text>
<text class="d-save" v-if="item.save"> {{ item.save }}</text>
</view>
</view>
</view>
<!-- 底部 --> <!-- 权益清单 -->
<text class="footer-text">如有问题请联系客服 · 支持 7 天无理由退款</text> <view class="benefits-section">
<text class="section-title">权益对比</text>
<view class="benefits-card">
<view
class="benefit-row"
v-for="(item, idx) in benefitList"
:key="idx"
>
<view class="benefit-left">
<text class="benefit-icon">{{ item.icon }}</text>
<text class="benefit-text">{{ item.text }}</text>
</view>
<view class="benefit-check">
<text class="check-mark" :class="{ 'pro-only': item.proOnly, 'vip-only': !item.proOnly }">
{{ item.proOnly ? 'Pro' : (item.hasPro ? 'Pro+VIP' : 'VIP') }}
</text>
</view>
</view>
</view>
</view>
<!-- 支付按钮 -->
<view class="pay-section">
<button
class="pay-btn"
:loading="loading"
:disabled="loading"
@click="handlePay"
>
<text class="pay-btn-text">立即支付 ¥{{ currentPrice }}</text>
</button>
<text class="pay-agreement">
点击支付即表示同意用户协议隐私政策
</text>
</view>
<view class="footer-spacer"></view>
</scroll-view>
</view> </view>
</template> </template>
@@ -85,32 +124,28 @@ import { useUserStore } from '@/stores/user'
const userStore = useUserStore() const userStore = useUserStore()
// 方案
const selectedPlan = ref('pro') const selectedPlan = ref('pro')
// 套餐时长
const durations = ref([
{ key: 'pro-monthly', label: 'Pro 月付', price: 19.9 },
{ key: 'pro-quarterly', label: 'Pro 季付', price: 49.9, save: '省 ¥10' },
{ key: 'vip-monthly', label: 'VIP 月付', price: 39.9 },
{ key: 'vip-quarterly', label: 'VIP 季付', price: 99.9, save: '省 ¥20' }
])
const selectedDuration = ref('pro-monthly') const selectedDuration = ref('pro-monthly')
// 权益 const durationOptions = computed(() => {
const benefits = ref([ const isPro = selectedPlan.value === 'pro'
{ icon: '🤖', text: 'AI 问答无限次' }, return [
{ icon: '📚', text: '全量知识内容解锁' }, { key: isPro ? 'pro-monthly' : 'vip-monthly', label: '月付', price: isPro ? 19.9 : 39.9 },
{ icon: '📡', text: '每日趋势优先推送' }, { key: isPro ? 'pro-quarterly' : 'vip-quarterly', label: '季付', price: isPro ? 49.9 : 99.9, save: isPro ? '¥10' : '¥20' }
{ icon: '⭐', text: '收藏夹无限容量' }, ]
{ icon: '🎁', text: '专属会员标识' }, })
{ icon: '💬', text: '专属客服支持' }
const benefitList = ref([
{ icon: '🤖', text: 'AI 问答无限次', proOnly: true, hasPro: true },
{ icon: '📚', text: '全量知识内容解锁', proOnly: true, hasPro: true },
{ icon: '📡', text: '每日趋势优先推送', proOnly: true, hasPro: true },
{ icon: '⭐', text: '收藏夹无限容量', proOnly: true, hasPro: true },
{ icon: '💬', text: '专属客服支持', proOnly: false, hasPro: false },
{ icon: '🎁', text: '专属会员标识', proOnly: false, hasPro: false }
]) ])
// 当前价格
const currentPrice = computed(() => { const currentPrice = computed(() => {
const item = durations.value.find(d => d.key === selectedDuration.value) const item = durationOptions.value.find(d => d.key === selectedDuration.value)
return item ? item.price : 19.9 return item ? item.price : 19.9
}) })
@@ -122,24 +157,14 @@ async function handlePay() {
return return
} }
// 找到对应的 productId const productId = selectedDuration.value
const productMap = {
'pro-monthly': 'pro-monthly',
'pro-quarterly': 'pro-quarterly',
'vip-monthly': 'vip-monthly',
'vip-quarterly': 'vip-quarterly'
}
loading.value = true loading.value = true
try { try {
// 调用后端创建订单 const data = await paymentApi.createOrder(productId, userStore.token)
const data = await paymentApi.createOrder(productMap[selectedDuration.value], userStore.token)
// 获取微信支付参数
const payParam = data.payParams || data.wechatParam || data.paymentData const payParam = data.payParams || data.wechatParam || data.paymentData
if (payParam) { if (payParam) {
// 调起微信支付
uni.requestPayment({ uni.requestPayment({
provider: 'wxpay', provider: 'wxpay',
timeStamp: payParam.timeStamp || payParam.timestamp, timeStamp: payParam.timeStamp || payParam.timestamp,
@@ -154,7 +179,7 @@ async function handlePay() {
}, 1500) }, 1500)
}, },
fail: (err) => { fail: (err) => {
if (err.errMsg.includes('cancel')) { if (err.errMsg && err.errMsg.includes('cancel')) {
uni.showToast({ title: '已取消支付', icon: 'none' }) uni.showToast({ title: '已取消支付', icon: 'none' })
} else { } else {
uni.showToast({ title: '支付失败,请重试', icon: 'none' }) uni.showToast({ title: '支付失败,请重试', icon: 'none' })
@@ -179,19 +204,20 @@ function goBack() {
<style lang="scss" scoped> <style lang="scss" scoped>
.page-payment { .page-payment {
min-height: 100vh; height: 100vh;
padding: 0 20px; display: flex;
flex-direction: column;
background: var(--bg-primary); background: var(--bg-primary);
} }
.status-bar-placeholder { height: 44px; } .status-bar-placeholder { height: 44px; }
// 导航
.nav-bar { .nav-bar {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
padding: 12px 0; padding: 12px 20px;
flex-shrink: 0;
} }
.nav-back { .nav-back {
@@ -214,23 +240,45 @@ function goBack() {
width: 30px; width: 30px;
} }
.scroll-area {
flex: 1;
padding: 0 20px;
}
// 头部 // 头部
.payment-header { .payment-header {
text-align: center; text-align: center;
padding: 10px 0 20px; padding: 10px 0 24px;
}
.header-icon-wrap {
width: 56px;
height: 56px;
border-radius: 16px;
background: linear-gradient(135deg, var(--dim1-color), var(--dim3-color));
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 16px;
box-shadow: 0 8px 24px rgba(99,102,241,0.25);
}
.header-icon {
font-size: 28px;
color: white;
} }
.header-title { .header-title {
font-size: 22px; font-size: 24px;
font-weight: 700; font-weight: 700;
color: var(--text-primary); color: var(--text-primary);
display: block; display: block;
margin-bottom: 6px;
} }
.header-desc { .header-desc {
font-size: 12px; font-size: 13px;
color: var(--text-tertiary); color: var(--text-tertiary);
margin-top: 6px;
display: block; display: block;
} }
@@ -238,181 +286,235 @@ function goBack() {
.plan-selector { .plan-selector {
display: flex; display: flex;
gap: 12px; gap: 12px;
margin-bottom: 20px; margin-bottom: 24px;
} }
.plan-tag { .plan-card {
flex: 1; flex: 1;
padding: 16px; padding: 20px 16px;
background: var(--glass-bg); background: var(--glass-bg);
border: 1px solid var(--glass-border); border: 1.5px solid var(--glass-border);
border-radius: 14px; border-radius: 16px;
text-align: center;
position: relative; position: relative;
transition: all 0.3s;
&.active {
border-color: var(--dim1-color);
box-shadow: 0 0 0 1px rgba(99,102,241,0.2), 0 8px 24px rgba(99,102,241,0.1);
background: linear-gradient(135deg, rgba(99,102,241,0.06), rgba(99,102,241,0.02));
}
&.vip.active {
border-color: #ffa726;
box-shadow: 0 0 0 1px rgba(255,167,38,0.2), 0 8px 24px rgba(255,167,38,0.1);
background: linear-gradient(135deg, rgba(255,167,38,0.06), rgba(255,167,38,0.02));
}
} }
.plan-tag.active { .plan-top {
background: linear-gradient(135deg, rgba(124, 77, 255, 0.15), rgba(0, 0, 0, 0.2)); display: flex;
border-color: rgba(124, 77, 255, 0.3); align-items: center;
} justify-content: space-between;
margin-bottom: 12px;
.plan-tag.vip.active {
background: linear-gradient(135deg, rgba(255, 152, 0, 0.15), rgba(0, 0, 0, 0.2));
border-color: rgba(255, 152, 0, 0.3);
} }
.plan-name { .plan-name {
font-size: 18px; font-size: 20px;
font-weight: 700; font-weight: 700;
color: var(--text-primary); color: var(--text-primary);
display: block;
margin-bottom: 6px;
}
.plan-price {
font-size: 24px;
font-weight: 700;
color: var(--dim1-color);
display: block;
}
.plan-tag.vip .plan-price {
color: #ffa726;
}
.unit {
font-size: 12px;
color: var(--text-tertiary);
font-weight: 400;
} }
.plan-badge { .plan-badge {
position: absolute;
top: -8px;
right: 12px;
background: linear-gradient(135deg, #ff9800, #ff5722);
color: white;
font-size: 10px; font-size: 10px;
font-weight: 600;
padding: 2px 10px; padding: 2px 10px;
border-radius: 10px; border-radius: 10px;
background: linear-gradient(135deg, var(--dim1-color), #7c4dff);
color: white;
} }
.plan-desc { .vip-badge {
font-size: 11px; background: linear-gradient(135deg, #ffa726, #ff7043);
color: var(--text-muted);
margin-top: 4px;
display: block;
} }
// 权益 .plan-price {
.benefits { font-size: 28px;
background: var(--glass-bg); font-weight: 700;
border: 1px solid var(--glass-border); color: var(--dim1-color);
border-radius: 14px; display: inline;
padding: 16px;
margin-bottom: 20px;
} }
.benefit-item { .plan-card.vip .plan-price {
color: #ffa726;
}
.plan-unit {
font-size: 12px;
color: var(--text-tertiary);
margin-left: 2px;
}
.plan-features {
margin-top: 14px;
display: flex; display: flex;
align-items: center; flex-direction: column;
gap: 10px; gap: 6px;
padding: 8px 0;
} }
.benefit-item:not(:last-child) { .plan-feature {
border-bottom: 1px solid var(--glass-border); font-size: 12px;
}
.b-icon { font-size: 16px; }
.b-text {
font-size: 13px;
color: var(--text-secondary); color: var(--text-secondary);
line-height: 1.5;
} }
// 套餐时长 // 套餐时长
.duration-section { .duration-section {
margin-bottom: 20px; margin-bottom: 24px;
} }
.duration-title { .section-title {
font-size: 14px; font-size: 14px;
color: rgba(255, 255, 255, 0.6); font-weight: 600;
margin-bottom: 10px; color: var(--text-primary);
display: block; display: block;
margin-bottom: 12px;
} }
.duration-list { .duration-list {
display: flex; display: flex;
gap: 8px; gap: 10px;
} }
.duration-item { .duration-item {
flex: 1; flex: 1;
padding: 12px 8px; padding: 14px 12px;
background: var(--glass-bg); background: var(--glass-bg);
border: 1px solid var(--glass-border); border: 1.5px solid var(--glass-border);
border-radius: 12px; border-radius: 14px;
text-align: center; text-align: center;
} transition: all 0.3s;
.duration-item.active { &.active {
background: rgba(124, 77, 255, 0.12); border-color: var(--dim1-color);
border-color: rgba(124, 77, 255, 0.3); background: linear-gradient(135deg, rgba(99,102,241,0.1), rgba(99,102,241,0.05));
}
} }
.d-label { .d-label {
font-size: 12px; font-size: 13px;
color: rgba(255, 255, 255, 0.6); color: var(--text-secondary);
display: block; display: block;
margin-bottom: 4px; margin-bottom: 6px;
} }
.d-price { .d-price {
font-size: 16px; font-size: 18px;
font-weight: 600; font-weight: 700;
color: var(--dim1-color); color: var(--text-primary);
display: block; display: block;
} }
.d-save { .d-save {
font-size: 10px; font-size: 11px;
font-weight: 600;
color: #4db6ac; color: #4db6ac;
display: block; display: block;
margin-top: 2px; margin-top: 4px;
}
// 权益对比
.benefits-section {
margin-bottom: 24px;
}
.benefits-card {
background: var(--glass-bg);
border: 1px solid var(--glass-border);
border-radius: 14px;
overflow: hidden;
}
.benefit-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 16px;
&:not(:last-child) {
border-bottom: 1px solid var(--glass-border);
}
}
.benefit-left {
display: flex;
align-items: center;
gap: 10px;
}
.benefit-icon {
font-size: 16px;
width: 24px;
text-align: center;
}
.benefit-text {
font-size: 13px;
color: var(--text-secondary);
}
.benefit-check {
.check-mark {
font-size: 11px;
font-weight: 600;
padding: 2px 10px;
border-radius: 8px;
&.pro-only {
background: rgba(99,102,241,0.1);
color: var(--dim1-color);
}
&.vip-only {
background: rgba(255,167,38,0.1);
color: #ffa726;
}
}
} }
// 支付 // 支付
.pay-section { .pay-section {
margin-top: 20px; margin-bottom: 24px;
} }
.pay-btn { .pay-btn {
width: 100%; width: 100%;
height: 50px; height: 52px;
background: linear-gradient(135deg, var(--dim1-color), var(--dim3-color)); background: linear-gradient(135deg, var(--dim1-color), var(--dim3-color));
color: white; color: white;
border: none; border: none;
border-radius: 12px; border-radius: 14px;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 6px 20px rgba(99,102,241,0.3);
&[disabled] {
opacity: 0.6;
}
}
.pay-btn-text {
font-size: 16px; font-size: 16px;
font-weight: 600; font-weight: 600;
box-shadow: 0 4px 16px rgba(179, 136, 255, 0.3);
} }
.pay-agreement { .pay-agreement {
font-size: 10px; font-size: 11px;
color: var(--text-muted); color: var(--text-muted);
text-align: center; text-align: center;
margin-top: 10px; margin-top: 12px;
display: block; display: block;
} }
.footer-text { .footer-spacer { height: 40px; }
font-size: 10px;
color: var(--text-muted);
text-align: center;
margin-top: 20px;
display: block;
}
</style> </style>
+2 -2
View File
@@ -39,9 +39,9 @@ async function sendVerificationCode(to, code) {
} }
try { try {
await transport.sendMail({ await transport.sendMail({
from: `"AI维度" <${SMTP_FROM}>`, from: `"宇之然AI维度" <${SMTP_FROM}>`,
to, to,
subject: '【AI维度】邮箱验证码', subject: '【宇之然AI维度】邮箱验证码',
html: ` html: `
<div style="max-width:480px;margin:0 auto;padding:24px;font-family:sans-serif;"> <div style="max-width:480px;margin:0 auto;padding:24px;font-family:sans-serif;">
<h2 style="color:#6366f1;">AI 维度 - 邮箱验证</h2> <h2 style="color:#6366f1;">AI 维度 - 邮箱验证</h2>