增强办公,新增审批,缺少电子签章功能

This commit is contained in:
2026-04-20 15:56:29 +08:00
parent a026ef7a54
commit cd71a3a457
18 changed files with 1791 additions and 537 deletions

View File

@@ -2,56 +2,175 @@
<view v-if="visible" class="mask" @tap="close">
<view class="sheet" @tap.stop>
<view class="sheet-header">
<text class="sheet-title">选择目的地</text>
<text class="sheet-title">选择城市</text>
<text class="sheet-close" @tap="close">关闭</text>
</view>
<view class="city-path">{{ pathLabel || '请选择目的地' }}</view>
<picker-view class="wheel" :value="pickerValue" @change="onChange">
<picker-view-column>
<view v-for="item in continents" :key="item.name" class="wheel-item"><text>{{ item.name }}</text></view>
</picker-view-column>
<picker-view-column>
<view v-for="item in countries" :key="item.name" class="wheel-item"><text>{{ item.name }}</text></view>
</picker-view-column>
<picker-view-column>
<view v-for="item in cities" :key="item" class="wheel-item"><text>{{ item }}</text></view>
</picker-view-column>
</picker-view>
<view class="footer">
<view class="tip-box">
<text class="tip-main">仅精确到城市即可</text>
<text class="tip-sub">如果列表里没有需要的城市可以先补录到上方再直接选择</text>
</view>
<view class="add-box">
<input v-model="newCountryName" class="add-input" type="text" placeholder="补录国家" />
<input v-model="newCityName" class="add-input" type="text" placeholder="补录城市" />
<button class="add-btn" :loading="adding" @tap="addNewCity">补录并新增</button>
</view>
<view class="city-path">{{ currentCountry && currentCity ? (currentCountry + ' / ' + currentCity) : '请选择城市' }}</view>
<view class="selector-wrap">
<picker-view class="picker-view" :value="pickerValue" @change="onPickerChange">
<picker-view-column>
<view v-for="item in countryOptions" :key="item.name" class="picker-item">
{{ item.name }}
</view>
</picker-view-column>
<picker-view-column>
<view v-for="item in currentCities" :key="item.cityId || item.cityName" class="picker-item">
{{ item.cityName }}
</view>
</picker-view-column>
</picker-view>
</view>
<view class="actions">
<button class="btn ghost" @tap="close">取消</button>
<button class="btn primary" @tap="confirm">确定</button>
<button class="btn primary" :disabled="!currentCity" @tap="confirm">确定</button>
</view>
</view>
</view>
</template>
<script>
const CITY_DATA = [
{ name: '亚洲', countries: [{ name: '中国', cities: ['北京', '上海', '广州', '深圳', '杭州', '成都'] }, { name: '日本', cities: ['东京', '大阪', '京都'] }, { name: '韩国', cities: ['首尔', '釜山'] }, { name: '新加坡', cities: ['新加坡'] }] },
{ name: '欧洲', countries: [{ name: '英国', cities: ['伦敦', '曼彻斯特'] }, { name: '法国', cities: ['巴黎', '里昂'] }, { name: '德国', cities: ['柏林', '慕尼黑'] }] },
{ name: '北美洲', countries: [{ name: '美国', cities: ['纽约', '洛杉矶', '旧金山'] }, { name: '加拿大', cities: ['多伦多', '温哥华'] }] },
{ name: '大洋洲', countries: [{ name: '澳大利亚', cities: ['悉尼', '墨尔本'] }, { name: '新西兰', cities: ['奥克兰', '惠灵顿'] }] }
]
import { listCities, addCity } from '@/api/fad/city'
export default {
props: { visible: Boolean, value: String },
emits: ['update:visible', 'change'],
data() { return { pickerValue: [0, 0, 0], cityData: CITY_DATA } },
computed: {
continents() { return this.cityData },
countries() { return this.continents[this.pickerValue[0]]?.countries || [] },
cities() { return this.countries[this.pickerValue[1]]?.cities || [] },
pathLabel() { const a = this.continents[this.pickerValue[0]]?.name || ''; const b = this.countries[this.pickerValue[1]]?.name || ''; const c = this.cities[this.pickerValue[2]] || ''; return [a,b,c].filter(Boolean).join(' / ') }
data() {
return {
loading: false,
adding: false,
countryOptions: [],
cityMap: {},
pickerValue: [0, 0],
currentCountry: '',
currentCity: '',
newCountryName: '',
newCityName: ''
}
},
computed: {
currentCities() {
return this.cityMap[this.currentCountry] || []
}
},
watch: {
visible(v) {
if (v) this.initValue()
}
},
watch: { visible(v) { if (v) this.initValue() } },
methods: {
initValue() { this.pickerValue = [0,0,0] },
onChange(e) { this.pickerValue = (e.detail.value || []).map(v => Number(v)) },
confirm() { this.$emit('change', this.pathLabel); this.close() },
close() { this.$emit('update:visible', false) }
async initValue() {
this.currentCountry = ''
this.currentCity = ''
this.newCountryName = ''
this.newCityName = ''
await this.loadOptions()
this.applyDefaultValue()
},
async loadOptions() {
this.loading = true
try {
const res = await listCities({ pageNum: 1, pageSize: 1000 })
const rows = res && (res.rows || (res.data && res.data.rows) || res.data || [])
const map = {}
const countries = []
rows.forEach(item => {
const country = item.countryName || ''
const city = item.cityName || ''
if (!country || !city) return
if (!map[country]) {
map[country] = []
countries.push({ name: country })
}
map[country].push(item)
})
this.countryOptions = countries
this.cityMap = map
} catch (e) {
uni.showToast({ title: e.message || '加载城市失败', icon: 'none' })
} finally {
this.loading = false
}
},
applyDefaultValue() {
const value = String(this.value || '')
for (let i = 0; i < this.countryOptions.length; i++) {
const country = this.countryOptions[i].name
const cityList = this.cityMap[country] || []
const j = cityList.findIndex(item => item.cityName === value)
if (j >= 0) {
this.pickerValue = [i, j]
this.currentCountry = country
this.currentCity = cityList[j].cityName
return
}
}
if (this.countryOptions.length) {
this.currentCountry = this.countryOptions[0].name
this.currentCity = (this.cityMap[this.currentCountry] || [])[0]?.cityName || ''
this.pickerValue = [0, 0]
}
},
close() {
this.$emit('update:visible', false)
},
onPickerChange(e) {
const value = (e.detail.value || []).map(v => Number(v))
const countryIndex = value[0] || 0
const country = this.countryOptions[countryIndex]?.name || ''
const cities = this.cityMap[country] || []
const cityIndex = Math.min(value[1] || 0, Math.max(cities.length - 1, 0))
this.pickerValue = [countryIndex, cityIndex]
this.currentCountry = country
this.currentCity = cities[cityIndex]?.cityName || ''
},
async addNewCity() {
const countryName = String(this.newCountryName || '').trim()
const cityName = String(this.newCityName || '').trim()
if (!countryName || !cityName) {
uni.showToast({ title: '请填写国家和城市', icon: 'none' })
return
}
this.adding = true
try {
await addCity({ countryName, cityName, status: 1, remark: '补录城市' })
uni.showToast({ title: '补录成功', icon: 'success' })
this.newCountryName = ''
this.newCityName = ''
await this.loadOptions()
this.currentCountry = countryName
this.currentCity = cityName
const countryIndex = this.countryOptions.findIndex(item => item.name === countryName)
const cityIndex = (this.cityMap[countryName] || []).findIndex(item => item.cityName === cityName)
this.pickerValue = [Math.max(countryIndex, 0), Math.max(cityIndex, 0)]
} catch (e) {
uni.showToast({ title: e.message || '补录失败', icon: 'none' })
} finally {
this.adding = false
}
},
confirm() {
if (!this.currentCity) return
this.$emit('change', this.currentCity)
this.close()
}
}
}
</script>
<style scoped>
.mask{position:fixed;inset:0;background:rgba(0,0,0,.45);z-index:1000;display:flex;align-items:flex-end}.sheet{width:100%;background:#fff;border-radius:24rpx 24rpx 0 0;padding:20rpx;box-sizing:border-box}.sheet-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:16rpx}.sheet-title{font-size:30rpx;font-weight:800}.sheet-close{font-size:26rpx;color:#1677ff}.city-path{padding:12rpx 16rpx;margin-bottom:12rpx;border-radius:16rpx;background:#f8fafc;color:#334155;font-size:24rpx}.wheel{height:520rpx}.wheel-item{display:flex;align-items:center;justify-content:center;height:88rpx}.footer{display:flex;gap:12rpx;margin-top:16rpx}.btn{flex:1;border-radius:16rpx}.btn.ghost{background:#f3f4f6;color:#334155}.btn.primary{background:#1677ff;color:#fff}
</style>
.mask{position:fixed;inset:0;background:rgba(0,0,0,.45);z-index:1000;display:flex;align-items:flex-end}.sheet{width:100%;background:#fff;border-radius:24rpx 24rpx 0 0;padding:20rpx;box-sizing:border-box}.sheet-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:16rpx}.sheet-title{font-size:30rpx;font-weight:800}.sheet-close{font-size:26rpx;color:#1677ff}.tip-box{padding:12rpx 16rpx;border-radius:16rpx;background:#eff6ff;margin-bottom:12rpx}.tip-main{display:block;font-size:26rpx;font-weight:700;color:#1d4ed8}.tip-sub{display:block;margin-top:6rpx;font-size:22rpx;color:#1e40af;line-height:1.5}.add-box{display:flex;gap:10rpx;flex-wrap:wrap;margin-bottom:12rpx}.add-input{flex:1;min-width:240rpx;height:72rpx;padding:0 16rpx;border:1px solid #e5e7eb;border-radius:16rpx;background:#fff}.add-btn{width:100%;height:72rpx;line-height:72rpx;border-radius:16rpx;background:#10b981;color:#fff;font-size:26rpx}.city-path{padding:12rpx 16rpx;margin-bottom:12rpx;border-radius:16rpx;background:#f8fafc;color:#334155;font-size:24rpx;line-height:1.5}.selector-wrap{height:420rpx;border:1px solid #eef2f7;border-radius:16rpx;overflow:hidden}.picker-view{height:100%}.picker-item{height:84rpx;line-height:84rpx;text-align:center;font-size:28rpx;color:#111827}.actions{display:flex;gap:12rpx;margin-top:16rpx}.btn{flex:1;border-radius:16rpx}.btn.ghost{background:#f3f4f6;color:#334155}.btn.primary{background:#1677ff;color:#fff}.btn.primary[disabled]{background:#9cc2ff;color:#fff}
</style>