Files
smt-used-car-h5/pages/index/myPublish.vue
T
2026-03-11 11:15:33 +08:00

908 lines
22 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="page">
<!-- 列表 -->
<z-paging ref="paging" v-model="list" @query="queryList" empty-view-text="暂无数据!">
<template #top>
<!-- 搜索抓取懂车帝 -->
<view class="dcd-search">
<u-search v-model="urls" placeholder="请输入抓取链接" :show-action="false" shape="round"></u-search>
<view class="bind-btn" @tap.stop="onBindByName">抓取</view>
</view>
</template>
<!-- <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 class="statussearch" @click="show=true">
<text class="year-text">{{status_name || '状态'}}</text>
<u-icon name="arrow-down" size="14" color="#999"></u-icon>
</view>
</view>
<u-picker :show="show" :columns="columns" keyName="label" @confirm="onstatus" @cancel="show = false"></u-picker>
</view>
<view class="list-wrap">
<uni-swipe-action-item
v-for="item in list"
:key="item.id"
:right-options="options1"
@click="swipeClick($event, item)"
>
<CarCard
:item="item"
:ismy="true"
@pdfchange="onSearch"
@request-price="onRequestPrice"
@restchange="onSearch"
/>
</uni-swipe-action-item>
</view>
</z-paging>
<uni-popup ref="inputDialog" type="dialog">
<view class="price-dialog">
<view class="pd-title">设置价格</view>
<view class="pd-row">
<view class="pd-label">贸易条款</view>
<view class="pd-price-line">
<uni-data-select
v-model="priceForm.trade_term"
class="tradeTerm"
:localdata="tradeTermOptions"
:clear="false"
/>
</view>
</view>
<view class="pd-row">
<view class="pd-label">价格</view>
<view class="pd-price-line">
<view class="pd-input-suffix-wrap">
<input
class="pd-input pd-input-price pd-input-with-suffix"
v-model="priceForm.price"
type="number"
inputmode="decimal"
placeholder="请输入数字"
@input="onPriceInput"
/>
<!-- 币种下拉:放在输入框内部右侧(无边框) -->
<view class="pd-currency-suffix">
<uni-data-select
class="pd-currency-select-suffix"
v-model="priceForm.currency"
:localdata="currencyOptions"
:clear="false"
/>
</view>
</view>
</view>
</view>
<!-- 语言单选 -->
<view class="pd-row">
<view class="pd-label">语言</view>
<radio-group class="pd-radio-group" @change="onLangChange">
<label class="pd-radio" v-for="l in langOptions" :key="l.value">
<radio :value="l.value" :checked="priceForm.lang === l.value" />
<text class="pd-radio-text">{{ l.text }}</text>
</label>
</radio-group>
</view>
<!-- 操作按钮 -->
<view class="pd-actions">
<view class="pd-btn pd-cancel" @tap="closePriceDialog">取消</view>
<view class="pd-btn pd-confirm" @tap="confirmPriceDialog">确认</view>
</view>
</view>
</uni-popup>
<!-- 抓取信息弹出层 -->
<uni-popup ref="crawlDialog" type="dialog">
<view class="crawl-dialog">
<view class="crawl-title">抓取信息</view>
<view class="crawl-content" v-if="crawlLoading">
<view class="crawl-loading">正在抓取中...</view>
</view>
<view class="crawl-content" v-else-if="crawlData">
<view class="crawl-item" v-if="crawlData.product_name">
<view class="crawl-label">车辆名称</view>
<view class="crawl-value">{{ crawlData.product_name }}</view>
</view>
<view class="crawl-item" v-if="crawlData.source_place">
<view class="crawl-label">车源地</view>
<view class="crawl-value">{{ crawlData.source_place }}</view>
</view>
<view class="crawl-item" v-if="crawlData.body_color">
<view class="crawl-label">车身颜色</view>
<view class="crawl-value">{{ crawlData.body_color }}</view>
</view>
<view class="crawl-item" v-if="crawlData.plate_time">
<view class="crawl-label">上牌时间</view>
<view class="crawl-value">{{ crawlData.plate_time }}</view>
</view>
<view class="crawl-item" v-if="crawlData.mileage">
<view class="crawl-label">里程</view>
<view class="crawl-value">{{ crawlData.mileage }}</view>
</view>
<view class="crawl-item" v-if="crawlData.price">
<view class="crawl-label">价格</view>
<view class="crawl-value">{{ crawlData.price }}</view>
</view>
<view class="crawl-item" v-if="crawlData.image_pull && crawlData.image_pull.length > 0">
<view class="crawl-label">图片</view>
<view class="crawl-images">
<image
v-for="(img, idx) in crawlData.image_pull"
:key="idx"
:src="img"
mode="aspectFill"
class="crawl-image"
/>
</view>
</view>
</view>
<view class="crawl-actions">
<view class="crawl-btn crawl-confirm1" @tap="closeCrawlDialog">关闭</view>
<view class="crawl-btn crawl-confirm" @tap="saveCrawlDialog">保存</view>
</view>
</view>
</uni-popup>
</view>
</template>
<script>
import { request } from '@/api/apiT.js'
import CarCard from '@/components/CarCard/CarCard.vue'
export default {
components: {
CarCard
},
data() {
return {
urls:'',
show: false,
status_id:1,
status_name:'',
crawlData: null,
crawlLoading: false,
columns: [
[{
label: '正常',
id: 1
}, {
label: '已售出',
id: 2
}, {
label: '已归档',
id: 3
}]
],
priceDialogItem: null,
priceDialogIndex: -1,
priceDialogValue: '',
options1: [{
text: '编辑',
style: {
backgroundColor: '#007aff'
}
}
],
keyword: '',
page: 1,
pageSize: 10,
loading: false,
finished: false,
list: [],
priceForm: {
trade_term:'',
price: "", // 只存数字字符串
currency: "USD", // USD | RUB | AED | EUR
lang: "en" // en | ru | es | ar
},
currencyOptions: [
{ value: "USD", text: "美元" },
{ value: "CNY", text: "人民币" },
{ value: "RUB", text: "卢布" },
{ value: "AED", text: "迪拉姆" },
{ value: "EUR", text: "欧元" }
],
langOptions: [
{ text: "英语", value: "en" },
{ text: "俄语", value: "ru" },
{ text: "西班牙语", value: "es" },
{ text: "阿拉伯语", value: "ar" }
],
tradeTermOptions: [
{ value: "FOB", text: "FOB" },
{ value: "CIF", text: "CIF" },
{ value: "EXW", text: "EXW" },
{ value: "DDP", text: "DDP" },
{ value: "CIP", text: "CIP" },
{ value: "FCA", text: "FCA" },
{ value: "CFR", text: "CFR" }
]
}
},
onLoad() {
this.initFeishuShare()
// H5:处理分享兜底参数 r(如果路由没命中/经历登录回跳,会先落到首页)
// #ifdef H5
try {
const u = new URL(window.location.href)
const r = u.searchParams.get('r')
if (r) {
const target = decodeURIComponent(r)
// 清理地址栏的 r,避免反复跳转
u.searchParams.delete('r')
window.history.replaceState({}, '', u.toString())
// 让首页加载完再跳
setTimeout(() => {
uni.navigateTo({ url: target })
}, 50)
}
} catch (e) {}
// #endif
},
methods: {
isFeishuEnv() {
// 飞书/ Lark 客户端内置浏览器 UA 通常包含 Feishu / Lark
try {
const ua = (navigator.userAgent || '').toLowerCase()
return ua.includes('feishu') || ua.includes('lark')
} catch (e) {
return false
}
},
loadFeishuJSSDK() {
// 动态加载飞书 H5 JS-SDK(避免你还要改 public/index.html)
return new Promise((resolve, reject) => {
try {
// 已加载
if (typeof window !== 'undefined' && window.h5sdk && window.tt) return resolve(true)
// H5 才有 document
if (typeof document === 'undefined') return reject(new Error('not in h5 environment'))
// 已插入但未 ready
const existed = document.querySelector('script[data-feishu-h5sdk="1"]')
if (existed) {
existed.addEventListener('load', () => resolve(true))
existed.addEventListener('error', () => reject(new Error('feishu sdk load error')))
return
}
const s = document.createElement('script')
s.async = true
s.src = 'https://lf-scm-cn.feishucdn.com/lark/op/h5-js-sdk-1.5.44.js'
s.onload = () => resolve(true)
s.onerror = () => reject(new Error('feishu sdk load error'))
document.head.appendChild(s)
} catch (e) {
reject(e)
}
})
},
getFeishuSignUrl() {
// 飞书签名通常要求“去掉 hash 的完整 URL”
try {
return window.location.href.split('#')[0]
} catch (e) {
return ''
}
},
initFeishuShare() {
try {
// 确保在飞书环境
// if (!this.isFeishuEnv()) return
// 动态加载 SDK
this.loadFeishuJSSDK()
// 这里需要后端提供签名接口:返回 { appId, timestamp, nonceStr, signature }
// 你可以按你后端习惯改成:request({ url: 'xxx', ... })
// const signUrl = encodeURIComponent(this.getFeishuSignUrl())
const signUrl = this.getFeishuSignUrl()
request({
url:'common/auth/feishu/signature',
method:'get',
data: {url:signUrl,app_id:uni.getStorageSync('fsappid')}
}).then(res => {
var cfg = res.data.data;
window.h5sdk.config({
appId: cfg.appId || uni.getStorageSync('fsappid'),
timestamp: cfg.timestamp,
nonceStr: cfg.nonceStr,
signature: cfg.signature,
jsApiList: ['shareWebContent','share']
})
window.h5sdk.ready(() => {
this._feishuReady = true
})
window.h5sdk.error((err) => {
this._feishuReady = false
console.error('Feishu JSSDK error:', err)
})
})
} catch (e) {
// 不要阻塞页面,失败就走 H5 的兜底分享
console.error('initFeishuShare failed:', e)
}
},
onRequestPrice(payload) {
// payload: { item, index }
this.priceDialogItem = payload && payload.item ? payload.item : null
this.priceDialogIndex = payload && typeof payload.index === 'number' ? payload.index : -1
this.priceDialogValue = ''
this.priceForm = {
trade_term:'',
price:"",
currency: "USD",
lang: "en"
}
this.$nextTick(() => {
this.$refs.inputDialog && this.$refs.inputDialog.open()
})
},
// H5 下载:用 a 标签触发
downloadByAnchor(url, filename) {
try {
const a = document.createElement('a')
a.href = url
a.download = filename || ''
a.target = '_blank'
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
} catch (e) {
// 兜底:直接打开
if (typeof window !== 'undefined') window.open(url, '_blank')
}
},
downloadpdf(item) {
request({
url: 'common/usedcar/pdf',
method: 'get',
data: { id: item.id }
}).then(res => {
if (res.data.code == 200) {
this.downloadByAnchor(res.data.data.pdf_url, res.data.data.file_name)
}
})
},
closePriceDialog() {
this.$refs.inputDialog.close()
},
// 只允许数字 + 可选1个小数点(你要“纯整数”也行,我后面给你一行改法)
onPriceInput(e) {
let v = (e.detail && e.detail.value) ? String(e.detail.value) : String(this.priceForm.price || "")
// 过滤掉非数字和点
v = v.replace(/[^\d.]/g, "")
// 只保留第一个小数点
const parts = v.split(".")
if (parts.length > 2) {
v = parts[0] + "." + parts.slice(1).join("")
}
// 去掉开头多余 0(保留 0.xxx)
v = v.replace(/^0+(\d)/, "$1")
if (v.startsWith(".")) v = "0" + v
this.priceForm.price = v
},
onCurrencyPickerChange(e) {
const idx = Number(e.detail.value || 0)
this.currencyIndex = idx
this.priceForm.currency = this.currencyOptions[idx]?.value || "USD"
},
onLangChange(e) {
this.priceForm.lang = e.detail.value
},
confirmPriceDialog() {
if (!this.priceForm.trade_term) {
return uni.showToast({ title: "请选择贸易条款", icon: "none" })
}
// 校验
if (!this.priceForm.price) {
return uni.showToast({ title: "请输入价格", icon: "none" })
}
const payload = {
trade_term:this.priceForm.trade_term,
price: this.priceForm.price, // 数字字符串
currency: this.priceForm.currency, // USD/RUB/AED/EUR
lang: this.priceForm.lang // en/ru/es/ar
}
console.log(payload)
this.closePriceDialog()
this.dialogInputConfirm(payload)
},
dialogInputConfirm(val) {
const item = this.priceDialogItem
const index = this.priceDialogIndex
if (!item) return
if (!val.price) {
uni.showToast({ title: '请输入价格', icon: 'none' })
return
}
request({
url: 'common/usedcar/produce/pdf',
method: 'get',
data: { id: item.id, price:val.price,currency:val.currency,lang:val.lang,trade_term:val.trade_term }
}).then(res => {
if (res.data.code == 200) {
// 通知刷新列表(你原来就是靠这个事件刷新)
this.onSearch()
this.downloadpdf(item)
} else {
uni.showToast({ title: res.data.msg || '生成失败', icon: 'none' })
}
})
},
onKeywordChange(val) {
// val 是输入值(uView 通常会传)
this.keyword = val
// 防抖:300ms 内持续输入只触发一次
if (this._kwTimer) clearTimeout(this._kwTimer)
this._kwTimer = setTimeout(() => {
this.onSearch()
}, 300)
},
onSearch() {
this.$refs.paging && this.$refs.paging.reload()
},
queryList(pageNo, pageSize, from) {
this.loading = true;
var that = this;
const params = {
current:pageNo,
status:this.status_id,
size:pageSize
};
params.keyword = this.keyword;
params.scene = 'self';
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();
//
})
},
onstatus(e){
if (e.value && e.value[0]) {
const selectedItem = typeof e.value[0] === 'object' ? e.value[0] : { value: e.value[0], label: e.value[0].toString() };
this.status_name = selectedItem.label;
this.status_id = selectedItem.id;
this.show = false;
this.$refs.paging && this.$refs.paging.reload()
}
},
swipeClick(e,item){
let {
content
} = e;
const id = item.id
uni.navigateTo({
url: `/pages/index/publishedit?id=${id}`
})
// 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}`
// })
// }
},
onBindByName(){
if (!this.urls || !this.urls.trim()) {
uni.showToast({ title: '请输入抓取链接', icon: 'none' })
return
}
this.crawlLoading = true
this.crawlData = null
this.$nextTick(() => {
this.$refs.crawlDialog && this.$refs.crawlDialog.open()
})
request({
url:'common/usedcar/url_crawl',
method:'get',
data: {url:this.urls}
}).then(res => {
this.crawlLoading = false
if (res.data.code == 200) {
this.crawlData = res.data.data || res.data
uni.showToast({ title: '抓取成功', icon: 'success' })
} else {
uni.showToast({ title: res.data.msg || '抓取失败', icon: 'none' })
this.crawlData = null
}
}).catch(err => {
this.crawlLoading = false
this.crawlData = null
console.error('抓取失败', err)
uni.showToast({ title: '抓取失败', icon: 'none' })
})
},
closeCrawlDialog() {
this.$refs.crawlDialog && this.$refs.crawlDialog.close()
this.crawlData = null
this.crawlLoading = false
},
saveCrawlDialog(){
function formatCnYearMonth(str) {
const match = str.match(/(\d{4})年(\d{1,2})月/)
if (!match) return str
const year = match[1]
const month = match[2].padStart(2, '0')
return `${year}-${month}-01`
}
const payload = {
...this.crawlData,
plate_time:formatCnYearMonth(this.crawlData.plate_time),
// images 数组 => "url1,url2,url3"
images: (this.crawlData.images || [])
.map(s => String(s || '').trim())
.filter(Boolean)
.join(',')
}
request({
url:'common/usedcar/store',
method:'post',
data:payload
}).then(res => {
this.$refs.crawlDialog && this.$refs.crawlDialog.close()
this.crawlData = null
this.crawlLoading = false
uni.showToast({
title: res.data.message,
icon: 'none'
});
setTimeout(() => {
this.$refs.paging && this.$refs.paging.reload()
}, 1000);
})
}
}
}
</script>
<style lang="scss" scoped>
.tradeTerm{
border: 1px solid #eae9e9;
border-radius: 6px;
}
.page {
min-height: 100vh;
background: #f4f6f8;
}
/* #ifdef H5 */
.page {
padding-bottom: calc(100rpx + env(safe-area-inset-bottom));
}
/* #endif */
.dcd-search{
display: flex;
align-items: center;
gap: 12px;
padding: 12px;
background: #fff;
}
.bind-btn {
padding: 8px 24px;
background: #3F435B;
color: #fff;
border-radius: 8px;
font-size: 28rpx;
white-space: nowrap;
cursor: pointer;
}
.bind-btn:active {
opacity: 0.8;
}
.page-head {
position: fixed;
width: 100%;
bottom: 0px;
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 {
display: flex;
align-items: center;
gap: 10px;
background: rgba(255, 255, 255, 1);
border-radius: 18rpx;
padding: 20rpx 10px;
box-shadow: 0 10rpx 28rpx rgba(0, 0, 0, 0.06);
}
.statussearch{
border: 1px solid #eae9e9;
border-radius: 4px;
padding: 4px 8px;
font-size: 24rpx;
display: flex;
align-items: center;
gap: 6px;
}
.list-wrap {
padding: 18rpx 24rpx 40rpx;
}
.loadmore {
padding: 18rpx 0 6rpx;
}
.price-dialog{
width: 640rpx;
background: #fff;
border-radius: 24rpx;
padding: 28rpx;
}
.pd-title{
font-size: 34rpx;
font-weight: 600;
color: #111;
margin-bottom: 20rpx;
text-align: center;
}
.pd-row{
display:flex;
align-items:center;
margin-top: 40rpx;
}
.pd-row-col{
flex-direction: column;
align-items: flex-start;
}
.pd-label{
width: 120rpx;
font-size: 28rpx;
color:#333;
}
.pd-input{
flex:1;
height: 66rpx;
border: 1px solid #eee;
border-radius: 16rpx;
padding: 0 18rpx;
font-size: 30rpx;
}
.pd-input-suffix-wrap{
position: relative;
flex: 1;
}
.pd-input-with-suffix{
padding-right: 180rpx; /* 给右侧币种留空间 */
}
.pd-currency-suffix{
position: absolute;
right: 14rpx;
top: 50%;
transform: translateY(-50%);
width: 140rpx;
height: 80rpx;
display: flex;
align-items: center;
justify-content: flex-end;
}
::v-deep .uni-select{
border: none !important;
}
.pd-radio-group{
display:flex;
flex-wrap: wrap;
gap: 16rpx 22rpx;
margin-top: 12rpx;
}
.pd-radio{
display:flex;
align-items:center;
}
.pd-radio-text{
margin-left: 10rpx;
font-size: 26rpx;
color:#333;
}
.pd-actions{
display:flex;
gap: 18rpx;
margin-top: 40rpx;
}
.pd-btn{
flex:1;
height: 84rpx;
border-radius: 18rpx;
display:flex;
align-items:center;
justify-content:center;
font-size: 30rpx;
}
.pd-cancel{
background:#f5f5f5;
color:#333;
}
.pd-confirm{
background:#3F435B; /* 你喜欢绿色风格 */
color:#fff;
font-weight:600;
}
.pd-price-line{
flex: 1;
display: flex;
align-items: center;
gap: 16rpx;
}
.pd-input-price{
flex: 1;
min-width: 240rpx;
}
/* 抓取信息弹出层样式 */
.crawl-dialog {
width: 680rpx;
max-height: 80vh;
background: #fff;
border-radius: 24rpx;
padding: 28rpx;
box-sizing: border-box;
overflow: hidden;
display: flex;
flex-direction: column;
}
.crawl-title {
font-size: 36rpx;
font-weight: 600;
color: #111;
margin-bottom: 30rpx;
text-align: center;
}
.crawl-content {
flex: 1;
overflow-y: auto;
max-height: 60vh;
}
.crawl-loading {
text-align: center;
padding: 60rpx 0;
font-size: 28rpx;
color: #999;
}
.crawl-item {
margin-bottom: 24rpx;
padding-bottom: 24rpx;
border-bottom: 1px solid #f0f0f0;
}
.crawl-item:last-child {
border-bottom: none;
margin-bottom: 0;
padding-bottom: 0;
}
.crawl-label {
font-size: 26rpx;
color: #999;
margin-bottom: 12rpx;
}
.crawl-value {
font-size: 30rpx;
color: #333;
word-break: break-all;
line-height: 1.6;
}
.crawl-images {
display: flex;
flex-wrap: wrap;
gap: 12rpx;
margin-top: 12rpx;
}
.crawl-image {
width: 180rpx;
height: 180rpx;
border-radius: 12rpx;
background: #f5f5f5;
}
.crawl-actions {
display: flex;
gap: 18rpx;
margin-top: 30rpx;
padding-top: 30rpx;
border-top: 1px solid #f0f0f0;
}
.crawl-btn {
flex: 1;
height: 84rpx;
border-radius: 18rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 30rpx;
}
.crawl-confirm1 {
border:1px solid #3F435B;
font-weight: 600;
}
.crawl-confirm {
background: #3F435B;
color: #fff;
font-weight: 600;
}
</style>