提交demo

This commit is contained in:
浅念
2026-01-06 10:32:46 +08:00
commit f768835f15
851 changed files with 102336 additions and 0 deletions
+332
View File
@@ -0,0 +1,332 @@
<template>
<view class="page">
<!-- 自定义导航(pages.json 里已设置 navigationStyle: custom) -->
<!-- <uni-nav-bar leftText=" " @clickLeft="goBack" leftIcon="back" :fixed="true" :border="false" color="#fff" backgroundColor="#3F435B" :statusBar="true" title="车辆详情"></uni-nav-bar> -->
<view class="content">
<!-- 基础信息卡片 -->
<view class="card">
<view class="title-row">
<text class="name">{{ detail.product_name }}</text>
</view>
<view class="kv">
<view class="kv-item" @click="copyIfHas('车辆识别码', detail.vin)">
<text class="k">车辆识别码</text>
<text class="v ellipsis">{{ detail.vin }}</text>
<text class="copy" v-if="detail.vin">复制</text>
</view>
<view class="kv-item">
<text class="k">车价(¥)</text>
<text class="v">{{ detail.price }}</text>
</view>
<view class="kv-item">
<text class="k">车源地</text>
<text class="v">{{ detail.source_place}}</text>
</view>
<view class="kv-item">
<text class="k">上牌地</text>
<text class="v">{{ detail.plate_place }}</text>
</view>
<view class="kv-item">
<text class="k">上牌时间</text>
<text class="v">{{ detail.plate_time }}</text>
</view>
<view class="kv-item">
<text class="k">排量</text>
<text class="v">{{ detail.displacement }}</text>
</view>
<view class="kv-item">
<text class="k">里程</text>
<text class="v">{{ detail.mileage }}</text>
</view>
<view class="kv-item">
<text class="k">车身颜色</text>
<text class="v">{{ detail.color }}</text>
</view>
<view class="kv-item">
<text class="k">创建时间</text>
<text class="v">{{ detail.created_at }}</text>
</view>
</view>
</view>
<view class="hero">
<view class="section-title">产品图</view>
<!-- 有图:九宫格/多行平铺;无图:占位 -->
<view class="img-grid">
<image
v-for="(src, i) in detail.images_pull"
:key="i"
class="grid-img"
:src="src"
mode="aspectFill"
@click="previewImages(i)"
/>
</view>
</view>
<!-- 车况介绍 -->
<view class="card">
<view class="section-title">车况介绍</view>
<text class="desc">{{ detail.condition_desc }}</text>
</view>
</view>
</view>
</template>
<script>
import { request, showToast } from '@/api/apiT.js'
export default {
data() {
return {
id: '',
detail: {},
currentImageIndex: 0
}
},
computed: {
images() {
// 兼容:images 可能是数组、逗号字符串、或后端不同字段
const raw =
this.detail.images_pull ||[]
if (Array.isArray(raw)) {
return raw.filter(Boolean)
}
if (typeof raw === 'string') {
return raw
.split(',')
.map(s => (s || '').trim())
.filter(Boolean)
}
return []
}
},
onLoad(options) {
// 1) 支持通过 id 打开:/pages/index/detils?id=123
this.id = options?.id || ''
this.getshows()
},
methods: {
//获取详情
getshows(){
let that = this;
request({
url:'common/usedcar/show',
method:'GET',
data:{
id:that.id
}
}).then(res => {
if (res.data.code == 200) {
this.detail = res.data.data
}
})
},
goBack() {
uni.navigateBack({
delta: 1,
fail: () => {
// 某些环境下没有历史栈,兜底回首页
uni.reLaunch({ url: '/pages/index/index' })
}
})
},
previewImages(startIndex) {
if (!this.images || this.images.length === 0) return
this.currentImageIndex = startIndex || 0
uni.previewImage({
current: this.images[this.currentImageIndex],
urls: this.images
})
}
}
}
</script>
<style scoped lang="scss">
.page {
min-height: 100vh;
background: #f6f7fb;
}
.content {
padding: 18rpx;
box-sizing: border-box;
}
.hero {
position: relative;
border-radius: 18rpx;
overflow: hidden;
margin-top: 16rpx;
background: #fff;
padding: 18rpx 18rpx;
box-shadow: 0 10rpx 30rpx rgba(0, 0, 0, 0.06);
}
.img-grid {
display: flex;
flex-wrap: wrap;
gap: 10rpx;
padding: 10rpx;
box-sizing: border-box;
}
.grid-img {
width: calc((100% - 20rpx) / 3);
height: 220rpx;
border-radius: 14rpx;
background: #f3f4f6;
}
.grid-img-single {
width: 100%;
height: 420rpx;
border-radius: 14rpx;
}
.hero-badges {
position: absolute;
left: 14rpx;
bottom: 14rpx;
display: flex;
gap: 10rpx;
}
.badge {
padding: 10rpx 14rpx;
border-radius: 999rpx;
font-size: 24rpx;
color: #fff;
background: rgba(255, 151, 47, 0.95);
}
.badge-ghost {
background: rgba(0, 0, 0, 0.4);
}
.card {
margin-top: 16rpx;
background: #fff;
border-radius: 18rpx;
padding: 18rpx 18rpx;
box-shadow: 0 10rpx 30rpx rgba(0, 0, 0, 0.05);
box-sizing: border-box;
}
.title-row {
display: flex;
align-items: center;
justify-content: space-between;
}
.name {
font-size: 34rpx;
font-weight: 700;
color: #111827;
line-height: 1.2;
}
.sub-row {
margin-top: 10rpx;
display: flex;
justify-content: space-between;
gap: 12rpx;
flex-wrap: wrap;
}
.sub {
font-size: 24rpx;
color: #6b7280;
}
.kv {
display: flex;
flex-direction: column;
}
.kv-item {
padding: 16rpx 0;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1rpx solid #f1f5f9;
}
.kv-item:last-child {
border-bottom: none;
padding-bottom: 0;
}
.k {
width: 180rpx;
font-size: 26rpx;
color: #374151;
flex-shrink: 0;
}
.v {
font-size: 26rpx;
color: #111827;
text-align: right;
flex: 1;
margin-left: 12rpx;
}
.ellipsis {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.copy {
margin-left: 12rpx;
font-size: 22rpx;
color: #ff972f;
flex-shrink: 0;
}
.section-title {
font-size: 28rpx;
font-weight: 700;
color: #111827;
margin-bottom: 10rpx;
}
.desc {
font-size: 26rpx;
color: #374151;
line-height: 1.7;
white-space: pre-wrap;
}
.footer {
padding: 28rpx 0 40rpx;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
gap: 14rpx;
}
.footer-text {
font-size: 24rpx;
color: #6b7280;
}
.retry {
padding: 14rpx 26rpx;
border-radius: 999rpx;
background: rgba(255, 151, 47, 0.12);
color: #ff972f;
font-size: 24rpx;
}
</style>
+249
View File
@@ -0,0 +1,249 @@
<template>
<view class="page">
<!-- 列表 -->
<z-paging ref="paging" v-model="list" @query="queryList" empty-view-text="暂无数据!">
<!-- <uni-nav-bar leftText=" " :fixed="true" :border="false" color="#fff" backgroundColor="#3F435B" :statusBar="true" title="首页"></uni-nav-bar> -->
<!-- 顶部:标题 + 搜索(不想要搜索可直接删除这一块) -->
<view class="page-head">
<view class="head-search">
<u-search
v-model="keyword"
placeholder="请输入关键词"
:show-action="false"
shape="round"
@change="onKeywordChange"
@search="onSearch"
@clear="onSearch"
/>
</view>
</view>
<view class="list-wrap">
<view
v-for="(item, idx) in list"
:key="item.id "
class="car-card"
@tap="goDetail(item)"
>
<image class="thumb" :src="item.images_pull[0]" mode="aspectFill" />
<view class="info">
<view class="row row1">
<text class="name u-line-1">{{ item.product_name }}</text>
</view>
<view class="row row2">
<view class="kv">
<text class="k">车源地</text>
<text class="v u-line-1">{{ item.source_place }}</text>
</view>
<view class="kv">
<text class="k">车身颜色</text>
<text class="v u-line-1">{{ item.body_color }}</text>
</view>
</view>
<view class="row row3">
<view class="kv">
<text class="k">上牌时间</text>
<text class="v">{{ ym(item.plate_time) }}</text>
</view>
<view class="kv">
<text class="k">里程</text>
<text class="v">{{ item.mileage }}</text>
</view>
</view>
</view>
</view>
</view>
</z-paging>
</view>
</template>
<script>
import { request } from '@/api/apiT.js'
export default {
data() {
return {
keyword: '',
page: 1,
pageSize: 10,
loading: false,
finished: false,
list: []
}
},
onLoad() {
},
methods: {
ym(dateStr) {
return String(dateStr || '').slice(0, 7) // "2026-01"
},
onKeywordChange(val) {
// val 是输入值(uView 通常会传)
this.keyword = val
// 防抖:300ms 内持续输入只触发一次
if (this._kwTimer) clearTimeout(this._kwTimer)
this._kwTimer = setTimeout(() => {
this.onSearch()
}, 300)
},
onSearch() {
this.queryList(this.page,this.pageSize)
},
queryList(pageNo, pageSize, from) {
this.loading = true;
var that = this;
const params = {
current:pageNo,
status:1,
size:10
};
params.keyword = this.keyword;
request({
url:'common/usedcar/list',
method:'get',
data: params
}).then(res => {
if(res.data.code ==200){
var datas = res.data.data.records;
this.$refs.paging.complete(datas);
}else{
}
uni.stopPullDownRefresh();
//
})
},
goDetail(item) {
// 你后续详情页如果要接 id,这里按后端字段改一下即可
const id = item.id
uni.navigateTo({
url: `/pages/index/detils?id=${id}`
})
},
formatMileage(v) {
if (v === '-' || v === undefined || v === null || String(v).trim() === '') return '-'
const n = Number(v)
if (isNaN(n)) return String(v)
// < 10000:直接显示;>= 10000:转万
if (n >= 10000) {
return `${(n / 10000).toFixed(1)}万 km`
}
return `${Math.round(n)} km`
}
}
}
</script>
<style lang="scss" scoped>
.page {
min-height: 100vh;
background: #f4f6f8;
}
.page-head {
position: fixed;
width: 100%;
bottom: 50px;
z-index: 99;
// padding: 28rpx 24rpx 18rpx;
// background: linear-gradient(180deg, rgba(255, 151, 47, 0.22) 0%, rgba(255, 151, 47, 0.06) 55%, #f4f6f8 100%);
}
.head-search {
background: rgba(255, 255, 255, 1);
border-radius: 18rpx;
padding: 20rpx 10px;
box-shadow: 0 10rpx 28rpx rgba(0, 0, 0, 0.06);
}
.list-wrap {
padding: 18rpx 24rpx 40rpx;
}
.car-card {
display: flex;
align-items: stretch;
padding: 18rpx;
border-radius: 18rpx;
background: rgba(255, 255, 255, 0.95);
box-shadow: 0 10rpx 30rpx rgba(0, 0, 0, 0.06);
margin-bottom: 18rpx;
}
.thumb {
width: 190rpx;
height: 140rpx;
border-radius: 14rpx;
background: #eef2f7;
flex-shrink: 0;
}
.info {
flex: 1;
margin-left: 18rpx;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.row {
display: flex;
align-items: center;
}
.row1 {
justify-content: space-between;
margin-bottom: 10rpx;
}
.name {
font-size: 30rpx;
font-weight: 700;
color: #111827;
max-width: 420rpx;
}
.tag {
font-size: 22rpx;
padding: 6rpx 12rpx;
border-radius: 999rpx;
background: rgba(255, 151, 47, 0.14);
color: #b45309;
flex-shrink: 0;
}
.row2,
.row3 {
justify-content: space-between;
gap: 16rpx;
}
.kv {
flex: 1;
min-width: 0;
display: flex;
align-items: baseline;
}
.k {
font-size: 22rpx;
color: #6b7280;
margin-right: 10rpx;
flex-shrink: 0;
}
.v {
font-size: 24rpx;
color: #374151;
font-weight: 600;
min-width: 0;
}
.loadmore {
padding: 18rpx 0 6rpx;
}
</style>
+346
View File
@@ -0,0 +1,346 @@
<template>
<view class="page">
<uni-nav-bar leftText=" " :fixed="true" :border="false" color="#fff" backgroundColor="#3F435B" :statusBar="true" title="我的"></uni-nav-bar>
<!-- 顶部:标题 + 搜索(不想要搜索可直接删除这一块) -->
<view class="page-head">
<view class="head-search">
<u-search
v-model="keyword"
placeholder="请输入关键词"
:show-action="false"
shape="round"
@search="onSearch"
@clear="onSearch"
/>
</view>
</view>
<!-- 列表 -->
<view class="list-wrap">
<uni-swipe-action-item
:threshold="0"
:right-options="options1"
@click="bindClick"
>
<view
v-for="(item, idx) in list"
:key="item.id || item.car_id || item.product_id || idx"
class="car-card"
@tap="goDetail(item)"
>
<image class="thumb" src="/static/image/car.png" mode="aspectFill" />
<view class="info">
<view class="row row1">
<text class="name u-line-1">{{ item.name }}</text>
</view>
<view class="row row2">
<view class="kv">
<text class="k">车源地</text>
<text class="v u-line-1">{{ item.source }}</text>
</view>
<view class="kv">
<text class="k">车身颜色</text>
<text class="v u-line-1">{{ item.color }}</text>
</view>
</view>
<view class="row row3">
<view class="kv">
<text class="k">上牌时间</text>
<text class="v">{{ item.plate_time }}</text>
</view>
<view class="kv">
<text class="k">里程</text>
<text class="v">{{ item.mileage }}</text>
</view>
</view>
</view>
</view>
</uni-swipe-action-item>
<!-- 空状态 -->
<u-empty v-if="!loading && list.length === 0" text="暂无数据" mode="data" />
<!-- 底部加载状态 -->
<view v-if="list.length > 0" class="loadmore">
<u-loadmore :status="loadStatus" />
</view>
</view>
</view>
</template>
<script>
import { request } from '@/api/apiT.js'
/**
* 首页列表接口(你如果后端接口不是这个路径,改这里即可)
* 兼容分页:page/per_page(或 page/page_size)
*/
const LIST_API = 'car/list'
export default {
data() {
return {
options1: [{
text: '编辑',
style: {
backgroundColor: '#007aff'
}
},
{
text: '删除',
style: {
backgroundColor: '#F56C6C'
}
}
],
keyword: '',
page: 1,
pageSize: 10,
loading: false,
finished: false,
list: [
{
name:'奥迪Q3 2013款 35 TFSI 舒适型',
source:'北京',
color:'黑色',
plate_time:'2013-07',
mileage:'11万公里'
}
]
}
},
computed: {
loadStatus() {
if (this.loading && this.page === 1) return 'loading'
if (this.loading) return 'loading'
if (this.finished) return 'nomore'
return 'loadmore'
}
},
onLoad() {
this.fetchList(true)
},
onPullDownRefresh() {
this.fetchList(true).finally(() => {
uni.stopPullDownRefresh()
})
},
onReachBottom() {
if (!this.loading && !this.finished) {
this.fetchList(false)
}
},
methods: {
onSearch() {
this.fetchList(true)
},
async fetchList(reset = false) {
if (this.loading) return
this.loading = true
try {
if (reset) {
this.page = 1
this.finished = false
}
const res = await request({
url: LIST_API,
method: 'GET',
data: {
page: this.page,
per_page: this.pageSize,
keyword: this.keyword || ''
}
})
const payload = (res && res.data) ? res.data : {}
const data = payload.data || payload || {}
// 兼容不同后端结构:list / data / items / rows / records
let rows =
data.list ||
data.data ||
data.items ||
data.rows ||
data.records ||
[]
if (!Array.isArray(rows)) {
rows = []
}
if (reset) this.list = []
this.list = this.list.concat(rows)
// 兼容 total / last_page / has_more 等字段
const hasMore =
(data.has_more === true) ||
(data.next_page_url ? true : false) ||
(false)
if (rows.length < this.pageSize && !hasMore) {
this.finished = true
} else {
this.page += 1
}
} catch (e) {
console.error('[home fetchList] error:', e)
uni.showToast({ title: e?.message || '加载失败', icon: 'none' })
} finally {
this.loading = false
}
},
goDetail(item) {
// 你后续详情页如果要接 id,这里按后端字段改一下即可
const id = item.id || item.car_id || item.product_id || ''
uni.navigateTo({
url: `/pages/index/detils?id=${encodeURIComponent(id)}`
})
},
formatPlate(v) {
if (!v || v === '-') return '-'
// 允许:时间戳 / yyyy-mm-dd / yyyy/mm/dd / yyyyMMdd
let d
if (typeof v === 'number' || /^\d+$/.test(String(v))) {
const n = Number(v)
// 秒/毫秒都兼容
d = new Date(n < 1e12 ? n * 1000 : n)
} else {
const s = String(v)
const s2 = s.replace(/\//g, '-').trim()
if (/^\d{8}$/.test(s2)) {
const y = s2.slice(0, 4)
const m = s2.slice(4, 6)
const dd = s2.slice(6, 8)
d = new Date(`${y}-${m}-${dd}T00:00:00`)
} else {
d = new Date(s2)
}
}
if (isNaN(d.getTime())) return String(v)
const y = d.getFullYear()
const m = String(d.getMonth() + 1).padStart(2, '0')
return `${y}-${m}`
},
formatMileage(v) {
if (v === '-' || v === undefined || v === null || String(v).trim() === '') return '-'
const n = Number(v)
if (isNaN(n)) return String(v)
// < 10000:直接显示;>= 10000:转万
if (n >= 10000) {
return `${(n / 10000).toFixed(1)}万 km`
}
return `${Math.round(n)} km`
}
}
}
</script>
<style lang="scss" scoped>
.page {
min-height: 100vh;
background: #f4f6f8;
}
.page-head {
// position: fixed;
// width: 100%;
}
.head-search {
background: rgba(255, 255, 255, 0.9);
// border-radius: 18rpx;
padding: 6rpx;
box-shadow: 0 10rpx 28rpx rgba(0, 0, 0, 0.06);
}
.list-wrap {
padding: 18rpx 24rpx 40rpx;
}
.car-card {
display: flex;
align-items: stretch;
padding: 18rpx;
border-radius: 18rpx;
background: rgba(255, 255, 255, 0.95);
box-shadow: 0 10rpx 30rpx rgba(0, 0, 0, 0.06);
// margin-bottom: 18rpx;
}
.thumb {
width: 190rpx;
height: 140rpx;
border-radius: 14rpx;
background: #eef2f7;
flex-shrink: 0;
}
.info {
flex: 1;
margin-left: 18rpx;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.row {
display: flex;
align-items: center;
}
.row1 {
justify-content: space-between;
margin-bottom: 10rpx;
}
.name {
font-size: 30rpx;
font-weight: 700;
color: #111827;
max-width: 420rpx;
}
.tag {
font-size: 22rpx;
padding: 6rpx 12rpx;
border-radius: 999rpx;
background: rgba(255, 151, 47, 0.14);
color: #b45309;
flex-shrink: 0;
}
.row2,
.row3 {
justify-content: space-between;
gap: 16rpx;
}
.kv {
flex: 1;
min-width: 0;
display: flex;
align-items: baseline;
}
.k {
font-size: 22rpx;
color: #6b7280;
margin-right: 10rpx;
flex-shrink: 0;
}
.v {
font-size: 24rpx;
color: #374151;
font-weight: 600;
min-width: 0;
}
.loadmore {
padding: 18rpx 0 6rpx;
}
</style>
+291
View File
@@ -0,0 +1,291 @@
<template>
<view class="page">
<!-- 列表 -->
<z-paging ref="paging" v-model="list" @query="queryList" empty-view-text="暂无数据!">
<!-- <uni-nav-bar leftText=" " :fixed="true" :border="false" color="#fff" backgroundColor="#3F435B" :statusBar="true" title="我的"></uni-nav-bar> -->
<!-- 顶部:标题 + 搜索(不想要搜索可直接删除这一块) -->
<view class="page-head">
<view class="head-search">
<u-search
v-model="keyword"
placeholder="请输入关键词"
:show-action="false"
shape="round"
@search="onSearch"
@clear="onSearch"
/>
</view>
</view>
<view class="list-wrap">
<uni-swipe-action-item
v-for="(item, idx) in list"
:key="item.id "
:right-options="options1"
@click="swipeClick($event, item)"
>
<view class="car-card" @click="goDetail(item)">
<image class="thumb" :src="item.images_pull[0]" mode="aspectFill" />
<view class="info">
<view class="row row1">
<text class="name u-line-1">{{ item.product_name }}</text>
</view>
<view class="row row2">
<view class="kv">
<text class="k">车源地</text>
<text class="v u-line-1">{{ item.source_place }}</text>
</view>
<view class="kv">
<text class="k">车身颜色</text>
<text class="v u-line-1">{{ item.body_color }}</text>
</view>
</view>
<view class="row row3">
<view class="kv">
<text class="k">上牌时间</text>
<text class="v">{{ ym(item.plate_time) }}</text>
</view>
<view class="kv">
<text class="k">里程</text>
<text class="v">{{ item.mileage }}</text>
</view>
</view>
</view>
</view>
</uni-swipe-action-item>
</view>
</z-paging>
</view>
</template>
<script>
import { request } from '@/api/apiT.js'
export default {
data() {
return {
options1: [{
text: '编辑',
style: {
backgroundColor: '#007aff'
}
},
{
text: '删除',
style: {
backgroundColor: '#F56C6C'
}
}
],
keyword: '',
page: 1,
pageSize: 10,
loading: false,
finished: false,
list: []
}
},
onLoad() {
},
methods: {
ym(dateStr) {
return String(dateStr || '').slice(0, 7) // "2026-01"
},
onSearch() {
this.queryList(this.page,this.pageSize)
},
queryList(pageNo, pageSize, from) {
this.loading = true;
var that = this;
const params = {
current:pageNo,
status:1,
size:10
};
params.scene = 'self';
params.keyword = this.keyword;
request({
url:'common/usedcar/list',
method:'get',
data: params
}).then(res => {
if(res.data.code ==200){
var datas = res.data.data.records;
this.$refs.paging.complete(datas);
}else{
}
uni.stopPullDownRefresh();
//
})
},
goDetail(item) {
// 你后续详情页如果要接 id,这里按后端字段改一下即可
const id = item.id || item.car_id || item.product_id || ''
uni.navigateTo({
url: `/pages/index/detils?id=${id}`
})
},
formatMileage(v) {
if (v === '-' || v === undefined || v === null || String(v).trim() === '') return '-'
const n = Number(v)
if (isNaN(n)) return String(v)
// < 10000:直接显示;>= 10000:转万
if (n >= 10000) {
return `${(n / 10000).toFixed(1)}万 km`
}
return `${Math.round(n)} km`
},
swipeClick(e,item){
let {
content
} = e;
if (content.text === '删除') {
uni.showModal({
title: '提示',
content: '是否删除',
success: res => {
if (res.confirm) {
request({
url:'common/usedcar/destroy',
method:'post',
data: {id:item.id}
}).then(res => {
uni.showToast({
title: res.data.message,
icon: 'none'
});
this.queryList()
})
} else if (res.cancel) {
console.log('用户点击取消');
}
}
});
} else {
const id = item.id
uni.navigateTo({
url: `/pages/index/publishedit?id=${id}`
})
}
}
}
}
</script>
<style lang="scss" scoped>
.page {
min-height: 100vh;
background: #f4f6f8;
}
.page-head {
position: fixed;
width: 100%;
bottom: 50px;
z-index: 99;
// padding: 28rpx 24rpx 18rpx;
// background: linear-gradient(180deg, rgba(255, 151, 47, 0.22) 0%, rgba(255, 151, 47, 0.06) 55%, #f4f6f8 100%);
}
.head-search {
background: rgba(255, 255, 255, 1);
border-radius: 18rpx;
padding: 20rpx 10px;
box-shadow: 0 10rpx 28rpx rgba(0, 0, 0, 0.06);
}
.list-wrap {
padding: 18rpx 24rpx 40rpx;
}
.car-card {
display: flex;
align-items: stretch;
padding: 18rpx;
border-radius: 18rpx;
background: rgba(255, 255, 255, 0.95);
box-shadow: 0 10rpx 30rpx rgba(0, 0, 0, 0.06);
// margin-bottom: 18rpx;
}
.uni-swipe{
margin-bottom: 18rpx;
}
.thumb {
width: 190rpx;
height: 140rpx;
border-radius: 14rpx;
background: #eef2f7;
flex-shrink: 0;
}
.info {
flex: 1;
margin-left: 18rpx;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.row {
display: flex;
align-items: center;
}
.row1 {
justify-content: space-between;
margin-bottom: 10rpx;
}
.name {
font-size: 30rpx;
font-weight: 700;
color: #111827;
max-width: 420rpx;
}
.tag {
font-size: 22rpx;
padding: 6rpx 12rpx;
border-radius: 999rpx;
background: rgba(255, 151, 47, 0.14);
color: #b45309;
flex-shrink: 0;
}
.row2,
.row3 {
justify-content: space-between;
gap: 16rpx;
}
.kv {
flex: 1;
min-width: 0;
display: flex;
align-items: baseline;
}
.k {
font-size: 22rpx;
color: #6b7280;
margin-right: 10rpx;
flex-shrink: 0;
}
.v {
font-size: 24rpx;
color: #374151;
font-weight: 600;
min-width: 0;
}
.loadmore {
padding: 18rpx 0 6rpx;
}
</style>
+321
View File
@@ -0,0 +1,321 @@
<template>
<view>
<!-- <uni-nav-bar leftText=" " :fixed="true" :border="false" color="#fff" backgroundColor="#3F435B" :statusBar="true" title="二手车录入"></uni-nav-bar> -->
<view class="content">
<view class="cell">
<view>产品名称 <text class="red">*</text></view>
<view class="right">
<input placeholder="请输入产品名称" v-model="carinfo.product_name" />
</view>
</view>
<view class="cell">
<view>车辆识别码</view>
<view class="right">
<input placeholder="请输入车架号/VIN码" v-model="carinfo.vin" />
</view>
</view>
<view class="cell">
<view>车价(¥)</view>
<view class="right">
<input placeholder="请输入车价" v-model="carinfo.price" />
</view>
</view>
<view class="cell">
<view>车源地</view>
<view class="right">
<input placeholder="请输入车源地" v-model="carinfo.source_place" />
</view>
</view>
<view class="cell">
<view>上牌地</view>
<view class="right">
<input placeholder="请输入上牌地" v-model="carinfo.plate_place" />
</view>
</view>
<view class="cell">
<view>上牌时间</view>
<view class="right">
<uni-datetime-picker
ref="borrowTimePicker"
type="date"
v-model="carinfo.plate_time"
placeholder="请选择时间"
:border="false"
:clear-icon="false"
@change="onBorrowTimeChange"
>
<view class="custom-datetime-input">
<text class="custom-datetime-text" :class="{ 'placeholder': !carinfo.plate_time }">
{{ carinfo.plate_time || '请选择时间' }}
</text>
<uni-icons type="calendar" size="16" color="#999"></uni-icons>
</view>
</uni-datetime-picker>
</view>
</view>
<view class="cell">
<view>排量</view>
<view class="right">
<input placeholder="请输入排量" v-model="carinfo.displacement" />
</view>
</view>
<view class="cell">
<view>里程</view>
<view class="right">
<input placeholder="请输入里程" v-model="carinfo.mileage" />
</view>
</view>
<view class="cell">
<view>车身颜色</view>
<view class="right">
<input placeholder="请输入车身颜色" v-model="carinfo.body_color" />
</view>
</view>
<view class="cont">
<view class="conts">产品图</view>
<view class="media-upload-wrapper">
<uni-file-picker ref="mediaPicker" limit="20" fileMediatype="image" @select="selectMedia" @delete="delfn" >
<view class="upload-icon-wrapper">
<image src="/static/image/img.png" mode="" class="addimg"></image>
</view>
</uni-file-picker>
</view>
</view>
<view class="cont">
<view class="conts">车况介绍</view>
<view class="">
<textarea auto-height v-model="carinfo.condition_desc" placeholder="请输入车况介绍"/>
</view>
</view>
</view>
<view class="zw">
<button class="btn" @click="draftadd">提交</button>
</view>
</view>
</template>
<script>
import { request,showToast } from '@/api/apiT.js'
export default {
data() {
return {
maxMedia:20,
carinfo:{
product_name:'',
vin:'',
price:'',
source_place:'',
plate_place:'',
plate_time:'',
displacement:'',
body_color:'',
images:[],
condition_desc:'',
mileage:''
}
}
},
onLoad() {
},
methods: {
onBorrowTimeChange(e) {
this.carinfo.plate_time = e
},
uploadfn(imgUrl){
return new Promise((resolve, reject) => {
uni.uploadFile({
url: 'https://usedcar.dev.hnxinyouhe.com/upload/image?scene=usedcarimg',
filePath: imgUrl,
name: 'image',
header:{'Fdesk-Auth-Token': uni.getStorageSync('token'),'Fdesk-Login-Type': 'feishu','Fdesk-System-Code':'fdesk'},
success: (uploadFileRes) => {
const res = JSON.parse(uploadFileRes.data || '{}')
if(res && res.data && res.data.href){
resolve(res.data.href)
}else{
reject('上传失败')
}
},
fail: (err) => reject(err)
});
})
},
selectMedia(e){
const allowCount = this.maxMedia - this.carinfo.images.length
const tempFilePaths = e.tempFilePaths || (e.tempFiles || []).map(item => item.path || item.url)
const paths = tempFilePaths.slice(0, allowCount)
if(paths.length === 0){
return
}
Promise.all(paths.map(p => this.uploadfn(p))).then(urls => {
urls.forEach(url => {
this.carinfo.images.push(url)
})
}).catch(() => {
uni.showToast({
title:'图片上传失败,请重试',
icon:'none'
})
})
},
delfn(e){
this.carinfo.images.splice(e.index, 1)
},
draftadd(){
const payload = {
...this.carinfo,
// images 数组 => "url1,url2,url3"
images: (this.carinfo.images || [])
.map(s => String(s || '').trim())
.filter(Boolean)
.join(',')
}
request({
url:'common/usedcar/store',
method:'post',
data:payload
}).then(res => {
uni.showToast({
title: res.data.message,
icon: 'none'
});
setTimeout(() => {
uni.reLaunch({
url: '/pages/index/index',// 替换为你要跳转的页面路径
});
}, 1000);
})
}
}
}
</script>
<style lang="scss">
.uniui-right{
position: absolute;
top: 2px;
}
.red{
color: red;
}
.content{
margin: 20px 12px;
background: #FFFFFF;
border-radius: 10px;
}
.zw{
height: 120px;
}
.cont{
position: relative;
padding: 18px 20px;
align-items: center;
font-weight: 500;
}
.conts{
margin-bottom: 20rpx;
}
.media-upload-wrapper{
display: flex;
flex-wrap: wrap;
gap: 16rpx;
}
.cell{
position: relative;
display: flex;
justify-content:space-between;
padding: 18px 20px;
/* border-bottom: 1px solid #C3C3C3; */
align-items: center;
font-weight: 500;
}
.cont::after,.cell::after{
content : '';
position : absolute;
width : 200%;
height : 200%;
top : 0;
left : 0;
transform-origin: 0 0;
border-width: 0;
border-bottom-width:1px;
border-style : solid;
transform : scale(0.5, 0.5);
border-radius : 1px;
box-sizing : border-box;
pointer-events : none;
border-color:#C3C3C3;
}
.cell1{
display: flex;
justify-content:space-between;
padding: 18px 20px;
align-items: center;
font-weight: 500;
}
.right{
text-align: right;
}
.select{
display: flex;
position: relative;
margin-right: 10px;
}
.foot{
position: fixed;
bottom: 0px;
width: 100%;
height: 120px;
background: #FFFFFF;
z-index: 2;
}
.btn{
background-color: #3F435B;
color: white;
border-radius: 50px;
width: 92%;
margin-bottom: 40px;
margin: 15px 4%;
}
.actives{
background-color: #dfdfdf;
}
.text{
display: flex;
align-items: center;
justify-content: space-between;
line-height: 30px;
padding: 10px 15px 0px;
.qd{
color: #3471FF;
}
}
.popup-content {
padding: 20px;
background-color: #fff;
}
.option-list {
max-height: 400rpx;
margin-top: 20rpx;
}
.option-item {
padding: 20rpx;
border-bottom: 1rpx solid #eee;
}
.option-item:last-child {
border-bottom: none;
}
.no-data {
text-align: center;
padding: 20rpx;
color: #999;
}
.addimg{
width: 215rpx;
height: 215rpx;
}
</style>
+422
View File
@@ -0,0 +1,422 @@
<template>
<view>
<!-- <uni-nav-bar leftText=" " @clickLeft="leftClick" leftIcon="back" :fixed="true" :border="false" color="#fff" backgroundColor="#3F435B" :statusBar="true" title="车辆编辑"></uni-nav-bar> -->
<view class="content">
<view class="cell">
<view>产品名称 <text class="red">*</text></view>
<view class="right">
<input placeholder="请输入产品名称" v-model="carinfo.product_name" />
</view>
</view>
<view class="cell">
<view>车辆识别码</view>
<view class="right">
<input placeholder="请输入车架号/VIN码" v-model="carinfo.vin" />
</view>
</view>
<view class="cell">
<view>车价(¥)</view>
<view class="right">
<input placeholder="请输入车价" v-model="carinfo.price" />
</view>
</view>
<view class="cell">
<view>车源地</view>
<view class="right">
<input placeholder="请输入车源地" v-model="carinfo.source_place" />
</view>
</view>
<view class="cell">
<view>上牌地</view>
<view class="right">
<input placeholder="请输入上牌地" v-model="carinfo.plate_place" />
</view>
</view>
<view class="cell">
<view>上牌时间</view>
<view class="right">
<uni-datetime-picker
ref="borrowTimePicker"
type="date"
v-model="carinfo.plate_time"
placeholder="请选择时间"
:border="false"
:clear-icon="false"
@change="onBorrowTimeChange"
>
<view class="custom-datetime-input">
<text class="custom-datetime-text" :class="{ 'placeholder': !carinfo.plate_time }">
{{ carinfo.plate_time || '请选择时间' }}
</text>
<uni-icons type="calendar" size="16" color="#999"></uni-icons>
</view>
</uni-datetime-picker>
</view>
</view>
<view class="cell">
<view>排量</view>
<view class="right">
<input placeholder="请输入排量" v-model="carinfo.displacement" />
</view>
</view>
<view class="cell">
<view>里程</view>
<view class="right">
<input placeholder="请输入里程" v-model="carinfo.mileage" />
</view>
</view>
<view class="cell">
<view>车身颜色</view>
<view class="right">
<input placeholder="请输入车身颜色" v-model="carinfo.body_color" />
</view>
</view>
<view class="cont">
<view class="conts">产品图</view>
<view class="media-upload-wrapper">
<uni-file-picker
ref="mediaPicker"
v-model="fileList"
:limit="maxMedia"
file-mediatype="image"
mode="grid"
:auto-upload="false"
@select="selectMedia"
@delete="delfn"
>
<view class="upload-icon-wrapper">
<image src="/static/image/img.png" mode="" class="addimg"></image>
</view>
</uni-file-picker>
</view>
</view>
<view class="cont">
<view class="conts">车况介绍</view>
<view class="">
<textarea auto-height v-model="carinfo.condition_desc" placeholder="请输入车况介绍"/>
</view>
</view>
</view>
<view class="zw">
<button class="btn" @click="draftadd">提交</button>
</view>
</view>
</template>
<script>
import { request,showToast } from '@/api/apiT.js'
export default {
data() {
return {
maxMedia:20,
// uni-file-picker 回显/展示用(url/name/extname)
fileList: [],
id:'',
_uploading:false,
carinfo:{
id:'',
product_name:'',
vin:'',
price:'',
source_place:'',
plate_place:'',
plate_time:'',
displacement:'',
body_color:'',
// 提交给后端用:相对路径数组(最终 join(','))
images:[],
condition_desc:'',
mileage:''
}
}
},
onLoad(options) {
this.id = options?.id || ''
this.getshows()
},
methods: {
leftClick() {
uni.navigateBack()
},
getshows(){
let that = this;
request({
url:'common/usedcar/show',
method:'GET',
data:{
id:that.id
}
}).then(res => {
if (res.data.code == 200) {
const d = res.data.data || {}
// 1) images:后端返回的是逗号字符串,这里转成数组(用于编辑提交)
const keys = String(d.images || '')
.split(',')
.map(s => String(s || '').trim())
.filter(Boolean)
// 2) 表单字段
this.carinfo = {
...this.carinfo,
...d,
images: keys
}
// 3) 图片回显:优先用 images_pull(签名URL用于显示)
const pulls = Array.isArray(d.images_pull) && d.images_pull.length
? d.images_pull
: keys
this.fileList = pulls.map((u, idx) => {
const raw = String(u || '')
const filename = raw.split('?')[0].split('/').pop() || `img_${idx + 1}`
const ext = filename.includes('.') ? filename.split('.').pop() : ''
return { url: raw, name: filename, extname: ext }
})
}
})
},
onBorrowTimeChange(e) {
this.carinfo.plate_time = e
},
uploadfn(imgUrl){
return new Promise((resolve, reject) => {
uni.uploadFile({
url: 'https://usedcar.dev.hnxinyouhe.com/upload/image?scene=usedcarimg',
filePath: imgUrl,
name: 'image',
header:{'Fdesk-Auth-Token': uni.getStorageSync('token'),'Fdesk-Login-Type': 'feishu','Fdesk-System-Code':'fdesk'},
success: (uploadFileRes) => {
const res = JSON.parse(uploadFileRes.data || '{}')
if(res && res.data && res.data.href){
resolve(res.data.href)
}else{
reject('上传失败')
}
},
fail: (err) => reject(err)
});
})
},
selectMedia(e){
const allowCount = this.maxMedia - (this.carinfo.images?.length || 0)
if(allowCount <= 0){
uni.showToast({
title:`最多只能上传${this.maxMedia}张图片`,
icon:'none'
})
return
}
const tempFiles = e.tempFiles || []
const tempFilePaths = (tempFiles.length
? tempFiles.map(f => f.path || f.url).filter(Boolean)
: (e.tempFilePaths || [])
)
const paths = tempFilePaths.slice(0, allowCount)
if(paths.length === 0) return
if(this._uploading) return
this._uploading = true
// 先占位,保证 delete 的 index 与 images 数组对得上
const startAt = this.carinfo.images.length
for(let i=0;i<paths.length;i++){
this.carinfo.images.push('__PENDING__')
}
Promise.all(paths.map(p => this.uploadfn(p)))
.then(keys => {
keys.forEach((key, i) => {
// 替换占位
this.$set(this.carinfo.images, startAt + i, String(key))
})
})
.catch(() => {
// 失败:移除占位,同时把 fileList 对应项删掉(避免页面“假回显”)
this.carinfo.images.splice(startAt, paths.length)
if(Array.isArray(this.fileList) && this.fileList.length >= startAt + paths.length){
this.fileList.splice(startAt, paths.length)
}
uni.showToast({
title:'图片上传失败,请重试',
icon:'none'
})
})
.finally(() => {
this._uploading = false
})
},
delfn(e){
// fileList 会由 uni-file-picker 自己维护;这里同步删除提交用的 images
this.carinfo.images.splice(e.index, 1)
},
draftadd(){
const payload = {
...this.carinfo,
// images 数组 => "url1,url2,url3"
images: (this.carinfo.images || [])
.map(s => String(s || '').trim())
.filter(s => !!s && s !== '__PENDING__')
.join(',')
}
request({
url:'common/usedcar/update',
method:'post',
data:{
id:payload.id,
product_name:payload.product_name,
vin:payload.vin,
price:payload.price,
source_place:payload.source_place,
plate_place:payload.plate_place,
plate_time:payload.plate_time,
displacement:payload.displacement,
body_color:payload.body_color,
images:payload.images,
condition_desc:payload.condition_desc,
mileage:payload.mileage
}
}).then(res => {
uni.showToast({
title: res.data.message,
icon: 'none'
});
setTimeout(() => {
uni.reLaunch({
url: '/pages/index/mine',// 替换为你要跳转的页面路径
});
}, 1000);
})
}
}
}
</script>
<style lang="scss">
.uniui-right{
position: absolute;
top: 2px;
}
.red{
color: red;
}
.content{
margin: 20px 12px;
background: #FFFFFF;
border-radius: 10px;
}
.zw{
height: 120px;
}
.cont{
position: relative;
padding: 18px 20px;
align-items: center;
font-weight: 500;
}
.conts{
margin-bottom: 20rpx;
}
.media-upload-wrapper{
display: flex;
flex-wrap: wrap;
gap: 16rpx;
}
.cell{
position: relative;
display: flex;
justify-content:space-between;
padding: 18px 20px;
/* border-bottom: 1px solid #C3C3C3; */
align-items: center;
font-weight: 500;
}
.cont::after,.cell::after{
content : '';
position : absolute;
width : 200%;
height : 200%;
top : 0;
left : 0;
transform-origin: 0 0;
border-width: 0;
border-bottom-width:1px;
border-style : solid;
transform : scale(0.5, 0.5);
border-radius : 1px;
box-sizing : border-box;
pointer-events : none;
border-color:#C3C3C3;
}
.cell1{
display: flex;
justify-content:space-between;
padding: 18px 20px;
align-items: center;
font-weight: 500;
}
.right{
text-align: right;
}
.select{
display: flex;
position: relative;
margin-right: 10px;
}
.foot{
position: fixed;
bottom: 0px;
width: 100%;
height: 120px;
background: #FFFFFF;
z-index: 2;
}
.btn{
background-color: #3F435B;
color: white;
border-radius: 50px;
width: 92%;
margin-bottom: 40px;
margin: 15px 4%;
}
.actives{
background-color: #dfdfdf;
}
.text{
display: flex;
align-items: center;
justify-content: space-between;
line-height: 30px;
padding: 10px 15px 0px;
.qd{
color: #3471FF;
}
}
.popup-content {
padding: 20px;
background-color: #fff;
}
.option-list {
max-height: 400rpx;
margin-top: 20rpx;
}
.option-item {
padding: 20rpx;
border-bottom: 1rpx solid #eee;
}
.option-item:last-child {
border-bottom: none;
}
.no-data {
text-align: center;
padding: 20rpx;
color: #999;
}
.addimg{
width: 215rpx;
height: 215rpx;
}
</style>
+226
View File
@@ -0,0 +1,226 @@
<template>
<view class="auth-page">
<view class="auth-card">
<view v-if="status === 'loading'">
<view class="spinner"></view>
<view class="text-wrap">
<text class="title">{{ title }}</text>
<text class="desc">{{ desc }}<text class="dots">{{ dots }}</text></text>
</view>
<view class="progress">
<view class="bar"></view>
</view>
</view>
<view v-else class="error-wrap">
<uni-icons type="clear" size="44" color="#ff4d4f"></uni-icons>
<text class="title">登录失败</text>
<text class="desc">{{ errorMsg }}</text>
<button class="retry-btn" @click="retry">重试</button>
</view>
</view>
</view>
</template>
<script>
import { request, showToast } from '@/api/apiT.js'
export default {
data() {
return {
status: 'loading',
title: '正在完成登录',
desc: '处理中',
dots: '',
errorMsg: '请稍后重试',
_dotTimer: null,
minStayMs: 600,
_startedAt: 0,
_code: ''
}
},
onLoad(options) {
this._startedAt = Date.now()
this.startDots()
// #ifdef H5
this._code = options.code || ''
// #endif
if (!this._code) {
this.fail('缺少 code 参数')
return
}
this.logins(this._code)
},
onUnload() {
this.stopDots()
},
methods: {
startDots() {
this.stopDots()
let n = 0
this._dotTimer = setInterval(() => {
n = (n + 1) % 4
this.dots = '.'.repeat(n)
}, 350)
},
stopDots() {
if (this._dotTimer) {
clearInterval(this._dotTimer)
this._dotTimer = null
}
},
fail(msg) {
this.status = 'error'
this.errorMsg = msg || '请稍后重试'
uni.showToast({ title: msg || '登录失败', icon: 'none' })
},
async logins(code) {
try {
this.title = '正在交换登录凭证'
this.desc = '请稍候'
const res = await request({
url: 'common/auth/login',
method: 'POST',
data: { feishu_code: code,app_id:uni.getStorageSync('fsappid')}
})
const token = res?.data?.data?.union_id
if (!token) {
throw new Error(res?.data?.msg || '未获取到 access_token')
}
uni.setStorageSync('token', token)
this.title = '登录成功'
this.desc = '正在进入首页'
this.goHomeSmooth()
} catch (err) {
console.error('[feishu callback] error:', err)
this.fail(err?.message || '登录失败')
}
},
goHomeSmooth() {
const elapsed = Date.now() - this._startedAt
const wait = Math.max(0, this.minStayMs - elapsed)
setTimeout(() => {
uni.reLaunch({ url: '/pages/index/index' })
}, wait)
},
retry() {
this.status = 'loading'
this.title = '正在完成登录'
this.desc = '处理中'
this.startDots()
if (this._code) this.logins(this._code)
else this.fail('缺少 code 参数')
}
}
}
</script>
<style scoped lang="scss">
.auth-page {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 40rpx;
box-sizing: border-box;
background: linear-gradient(180deg, #FF972F 0%, #F4F6F8 38%, #F4F6F8 100%);
}
.auth-card {
width: 100%;
max-width: 640rpx;
background: rgba(255, 255, 255, 0.92);
border-radius: 20rpx;
padding: 44rpx 36rpx 32rpx;
box-shadow: 0 10rpx 40rpx rgba(0, 0, 0, 0.08);
display: flex;
flex-direction: column;
align-items: center;
backdrop-filter: blur(10px);
}
.spinner {
width: 88rpx;
height: 88rpx;
border-radius: 50%;
border: 8rpx solid rgba(255,151,47, 0.18);
border-top-color: #FF972F;
animation: spin 0.9s linear infinite;
margin-bottom: 24rpx;
}
.text-wrap {
text-align: center;
}
.title {
display: block;
font-size: 34rpx;
color: #1f2937;
font-weight: 600;
margin-bottom: 8rpx;
}
.desc {
display: block;
font-size: 26rpx;
color: #6b7280;
}
.dots {
display: inline-block;
width: 40rpx;
text-align: left;
}
.progress {
width: 100%;
height: 10rpx;
border-radius: 999rpx;
background: rgba(255,151,47, 0.10);
overflow: hidden;
margin-top: 26rpx;
}
.progress .bar {
height: 100%;
width: 40%;
border-radius: 999rpx;
background: rgba(255,151,47, 0.55);
animation: move 1.1s ease-in-out infinite;
}
.error-wrap {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
}
.retry-btn {
margin-top: 24rpx;
width: 240rpx;
height: 72rpx;
line-height: 72rpx;
border-radius: 999rpx;
background: #FF972F;
color: #fff;
font-size: 28rpx;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
@keyframes move {
0% { transform: translateX(-60%); }
50% { transform: translateX(140%); }
100% { transform: translateX(-60%); }
}
</style>
+207
View File
@@ -0,0 +1,207 @@
<template>
<view class="auth-page">
<view class="auth-card">
<!-- 这里如果你有站点 logo,可替换为自己的路径;没有也不影响 -->
<!-- <image class="logo" src="/static/logo.png" mode="heightFix"></image> -->
<view class="spinner"></view>
<view class="text-wrap">
<text class="title">{{ title }}</text>
<text class="desc">{{ desc }}<text class="dots">{{ dots }}</text></text>
</view>
<view class="progress">
<view class="bar"></view>
</view>
</view>
</view>
</template>
<script>
import { request, showToast } from '@/api/apiT.js'
export default {
data() {
return {
fsappid: '',
title: '正在登录',
desc: '加载中',
dots: '',
_dotTimer: null,
// 最短停留时间,避免“闪一下就跳”的割裂感
minStayMs: 600,
_startedAt: 0
}
},
onLoad(options) {
this.fsappid = options.appid || '';
uni.setStorageSync('fsappid', this.fsappid);
this._startedAt = Date.now()
this.startDots()
this.silentAuth()
},
onUnload() {
this.stopDots()
},
methods: {
startDots() {
this.stopDots()
let n = 0
this._dotTimer = setInterval(() => {
n = (n + 1) % 4
this.dots = '.'.repeat(n)
}, 350)
},
stopDots() {
if (this._dotTimer) {
clearInterval(this._dotTimer)
this._dotTimer = null
}
},
async silentAuth() {
try {
// 1) 如果本地已有 token,直接进首页(丝滑)
const token = uni.getStorageSync('token')
if (token) {
this.title = '已登录'
this.desc = '正在进入首页'
return this.goHomeSmooth()
}
// 2) 获取飞书授权跳转地址(静默授权/免登)
this.title = '正在跳转飞书授权'
this.desc = '准备中'
const res = await request({
url: 'common/auth/feishu/scope',
method: 'get',
data:{app_id:this.fsappid}
})
const auth_url = res?.data?.data?.auth_url
if (!auth_url) {
throw new Error('未获取到授权地址')
}
// 3) 给一点点过渡时间,让动画先跑起来再跳转
await this.sleep(180)
// 仅 H5 才能用 window.location
// #ifdef H5
window.location.replace(auth_url)
// #endif
// #ifndef H5
showToast && showToast('仅 H5 环境支持飞书静默授权')
// #endif
} catch (err) {
console.error('[feishu silentAuth] error:', err)
uni.showToast({ title: err?.message || '登录失败', icon: 'none' })
}
},
goHomeSmooth() {
const elapsed = Date.now() - this._startedAt
const wait = Math.max(0, this.minStayMs - elapsed)
setTimeout(() => {
uni.reLaunch({
url: '/pages/index/index'
})
}, wait)
},
sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms))
}
}
}
</script>
<style scoped lang="scss">
.auth-page {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 40rpx;
box-sizing: border-box;
/* 更“高级”的登录背景 */
background: linear-gradient(180deg, #FF972F 0%, #F4F6F8 38%, #F4F6F8 100%);
}
.auth-card {
width: 100%;
max-width: 640rpx;
background: rgba(255, 255, 255, 0.92);
border-radius: 20rpx;
padding: 44rpx 36rpx 32rpx;
box-shadow: 0 10rpx 40rpx rgba(0, 0, 0, 0.08);
display: flex;
flex-direction: column;
align-items: center;
backdrop-filter: blur(10px);
}
.logo {
height: 72rpx;
margin-bottom: 26rpx;
}
.spinner {
width: 88rpx;
height: 88rpx;
border-radius: 50%;
border: 8rpx solid rgba(255,151,47, 0.18);
border-top-color: #FF972F;
animation: spin 0.9s linear infinite;
margin-bottom: 24rpx;
}
.text-wrap {
text-align: center;
}
.title {
display: block;
font-size: 34rpx;
color: #1f2937;
font-weight: 600;
margin-bottom: 8rpx;
}
.desc {
display: block;
font-size: 26rpx;
color: #6b7280;
}
.dots {
display: inline-block;
width: 40rpx; /* 固定宽度避免文字抖动 */
text-align: left;
}
.progress {
width: 100%;
height: 10rpx;
border-radius: 999rpx;
background: rgba(255, 151, 47, 0.10);
overflow: hidden;
margin-top: 26rpx;
}
.progress .bar {
height: 100%;
width: 40%;
border-radius: 999rpx;
background: rgba(255, 151, 47, 0.55);
animation: move 1.1s ease-in-out infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
@keyframes move {
0% { transform: translateX(-60%); }
50% { transform: translateX(140%); }
100% { transform: translateX(-60%); }
}
</style>