增强办公,新增审批,缺少电子签章功能
This commit is contained in:
@@ -42,6 +42,14 @@ export function getTodoTaskByBiz(bizType, bizId, assigneeUserId) {
|
||||
})
|
||||
}
|
||||
|
||||
export function getFlowTaskDetailByBiz(bizType, bizId) {
|
||||
return request({
|
||||
url: '/hrm/flow/task/detailByBiz',
|
||||
method: 'get',
|
||||
params: { bizType, bizId }
|
||||
})
|
||||
}
|
||||
|
||||
export function approveFlowTask(taskId, data) {
|
||||
return request({
|
||||
url: `/hrm/flow/task/${taskId}/approve`,
|
||||
|
||||
@@ -32,6 +32,13 @@ export function editTravelReq(data) {
|
||||
})
|
||||
}
|
||||
|
||||
export function earlyEndTravelReq(bizId) {
|
||||
return request({
|
||||
url: `/hrm/travel/earlyEnd/${bizId}`,
|
||||
method: 'put'
|
||||
})
|
||||
}
|
||||
|
||||
export function delTravelReq(bizIds) {
|
||||
return request({
|
||||
url: `/hrm/travel/${bizIds}`,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<text class="sheet-title">选择时间</text>
|
||||
<text class="sheet-close" @tap="close">关闭</text>
|
||||
</view>
|
||||
<picker-view ref="picker" class="wheel" @change="onChange">
|
||||
<picker-view ref="picker" class="wheel" :value="pickerValue" indicator-style="height: 88rpx;" @change="onChange">
|
||||
<picker-view-column>
|
||||
<view v-for="(d, i) in dateOptions" :key="d" class="wheel-item"><text>{{ d }}</text></view>
|
||||
</picker-view-column>
|
||||
@@ -30,31 +30,41 @@ export default {
|
||||
dateOptions() { const res = []; const base = new Date(); base.setHours(0,0,0,0); for (let i = 0; i < this.days; i++) { const d = new Date(base); d.setDate(d.getDate() + i); res.push(this.formatYMD(d)) } return res },
|
||||
hourOptions() { return Array.from({ length: 24 }, (_, i) => `${i < 10 ? '0' : ''}${i}时`) }
|
||||
},
|
||||
watch: {
|
||||
visible(v) { if (v) setTimeout(() => this.initValue(), 100) }
|
||||
watch: {
|
||||
visible(v) {
|
||||
if (v) {
|
||||
this.$nextTick(() => {
|
||||
setTimeout(() => this.initValue(), 50)
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
initValue() {
|
||||
if (this.value) {
|
||||
const v = String(this.value).replace('T', ' ')
|
||||
const parts = v.split(' ')
|
||||
const ymd = parts[0].split('-')
|
||||
const hm = parts[1]?.split(':') || [0, 0]
|
||||
const hour = parseInt(hm[0]) || 0
|
||||
const dateIndex = this.dateOptions.indexOf(ymd.join('-'))
|
||||
const ymd = (parts[0] || '').split('-')
|
||||
const hm = (parts[1] || '00:00:00').split(':')
|
||||
const hour = Math.max(0, Math.min(23, parseInt(hm[0], 10) || 0))
|
||||
const dateKey = ymd.length === 3 ? ymd.join('-') : this.dateOptions[0]
|
||||
const dateIndex = this.dateOptions.indexOf(dateKey)
|
||||
this.pickerValue = [dateIndex >= 0 ? dateIndex : 0, hour]
|
||||
} else {
|
||||
this.pickerValue = [0, 0]
|
||||
}
|
||||
this.$nextTick(() => {
|
||||
const pv = this.$refs.picker
|
||||
if (pv) pv.setValue(this.pickerValue)
|
||||
if (pv && pv.setValue) pv.setValue(this.pickerValue)
|
||||
})
|
||||
},
|
||||
onChange(e) { this.pickerValue = e.detail.value },
|
||||
onChange(e) {
|
||||
const val = e.detail.value || [0, 0]
|
||||
this.pickerValue = [Number(val[0]) || 0, Number(val[1]) || 0]
|
||||
},
|
||||
confirm() {
|
||||
const date = this.dateOptions[this.pickerValue[0]] || this.dateOptions[0]
|
||||
let hour = this.pickerValue[1] || 0
|
||||
let hour = Number(this.pickerValue[1]) || 0
|
||||
if (hour >= 24) hour = 23
|
||||
this.$emit('change', `${date}T${hour < 10 ? '0' : ''}${hour}:00:00.000+08:00`)
|
||||
this.close()
|
||||
|
||||
@@ -2,151 +2,171 @@
|
||||
<view v-if="visible" class="picker-mask" @tap="close">
|
||||
<view class="picker-sheet" @tap.stop>
|
||||
<view class="picker-header">
|
||||
<text class="picker-title">选择目的地</text>
|
||||
<text class="picker-title">选择城市</text>
|
||||
<text class="picker-close" @tap="close">关闭</text>
|
||||
</view>
|
||||
|
||||
<view class="city-path">{{ currentPath }}</view>
|
||||
<view class="tip-box">
|
||||
<text class="tip-main">仅精确到城市即可</text>
|
||||
<text class="tip-sub">如果列表里没有想要的城市,可以先在上方补录后再选择。</text>
|
||||
</view>
|
||||
|
||||
<picker-view class="city-wheel" :value="wheelValue" @change="onChange">
|
||||
<picker-view-column>
|
||||
<view class="wheel-item" v-for="item in continents" :key="item.name">
|
||||
<view class="wheel-value">{{ item.name }}</view>
|
||||
</view>
|
||||
</picker-view-column>
|
||||
<picker-view-column>
|
||||
<view class="wheel-item" v-for="item in currentCountries" :key="item.name">
|
||||
<view class="wheel-value">{{ item.name }}</view>
|
||||
</view>
|
||||
</picker-view-column>
|
||||
<picker-view-column>
|
||||
<view class="wheel-item" v-for="item in currentCities" :key="item.name">
|
||||
<view class="wheel-value">{{ item.name }}</view>
|
||||
</view>
|
||||
</picker-view-column>
|
||||
</picker-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="picker-footer">
|
||||
<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: '泰国', cities: ['曼谷', '清迈', '普吉'] },
|
||||
{ name: '马来西亚', cities: ['吉隆坡', '槟城', '新山'] },
|
||||
{ name: '印度', cities: ['新德里', '孟买', '班加罗尔'] },
|
||||
{ name: '阿联酋', cities: ['迪拜', '阿布扎比'] }
|
||||
]},
|
||||
{ name: '欧洲', countries: [
|
||||
{ name: '英国', cities: ['伦敦', '曼彻斯特', '伯明翰'] },
|
||||
{ name: '法国', cities: ['巴黎', '里昂', '马赛'] },
|
||||
{ name: '德国', cities: ['柏林', '慕尼黑', '法兰克福'] },
|
||||
{ name: '意大利', cities: ['罗马', '米兰', '威尼斯'] },
|
||||
{ name: '西班牙', cities: ['马德里', '巴塞罗那', '瓦伦西亚'] },
|
||||
{ name: '荷兰', cities: ['阿姆斯特丹', '鹿特丹'] }
|
||||
]},
|
||||
{ name: '北美洲', countries: [
|
||||
{ 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: ['约翰内斯堡', '开普敦'] },
|
||||
{ name: '肯尼亚', cities: ['内罗毕'] }
|
||||
]}
|
||||
]
|
||||
import { listCities, addCity } from '@/api/fad/city'
|
||||
|
||||
export default {
|
||||
props: { visible: Boolean, value: String },
|
||||
emits: ['update:visible', 'change'],
|
||||
data() {
|
||||
return {
|
||||
wheelValue: [0, 0, 0],
|
||||
data: CITY_DATA
|
||||
loading: false,
|
||||
adding: false,
|
||||
countryOptions: [],
|
||||
cityMap: {},
|
||||
pickerValue: [0, 0],
|
||||
currentCountry: '',
|
||||
currentCity: '',
|
||||
newCountryName: '',
|
||||
newCityName: ''
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
continents() { return this.data },
|
||||
currentCountries() { return this.continents[this.wheelValue[0]]?.countries || [] },
|
||||
currentCities() { return this.currentCountries[this.wheelValue[1]]?.cities?.map(name => ({ name })) || [] },
|
||||
currentPath() {
|
||||
const c1 = this.continents[this.wheelValue[0]]?.name || ''
|
||||
const c2 = this.currentCountries[this.wheelValue[1]]?.name || ''
|
||||
const c3 = this.currentCities[this.wheelValue[2]]?.name || ''
|
||||
return [c1, c2, c3].filter(Boolean).join(' / ') || '请选择目的地'
|
||||
currentCities() {
|
||||
return this.cityMap[this.currentCountry] || []
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
visible(val) {
|
||||
if (val) this.initFromValue()
|
||||
},
|
||||
wheelValue() {
|
||||
this.normalize()
|
||||
visible(v) {
|
||||
if (v) this.initValue()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
initFromValue() {
|
||||
const target = String(this.value || '')
|
||||
let found = [0, 0, 0]
|
||||
this.data.forEach((continent, i) => {
|
||||
continent.countries.forEach((country, j) => {
|
||||
country.cities.forEach((city, k) => {
|
||||
const full = `${continent.name}/${country.name}/${city}`
|
||||
if (target === full || target === city) found = [i, j, k]
|
||||
})
|
||||
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.wheelValue = found
|
||||
},
|
||||
normalize() {
|
||||
const c = this.continents[this.wheelValue[0]]
|
||||
if (!c) return
|
||||
if (this.wheelValue[1] >= c.countries.length) this.wheelValue[1] = 0
|
||||
const country = c.countries[this.wheelValue[1]]
|
||||
if (!country) return
|
||||
if (this.wheelValue[2] >= country.cities.length) this.wheelValue[2] = 0
|
||||
},
|
||||
onChange(e) {
|
||||
this.wheelValue = (e.detail.value || []).map(v => Number(v))
|
||||
},
|
||||
confirm() {
|
||||
const continent = this.continents[this.wheelValue[0]]
|
||||
const country = this.currentCountries[this.wheelValue[1]]
|
||||
const city = this.currentCities[this.wheelValue[2]]
|
||||
const payload = {
|
||||
continent: continent?.name || '',
|
||||
country: country?.name || '',
|
||||
city: city?.name || '',
|
||||
label: [continent?.name, country?.name, city?.name].filter(Boolean).join(' / '),
|
||||
value: [continent?.name, country?.name, city?.name].filter(Boolean).join('/')
|
||||
this.countryOptions = countries
|
||||
this.cityMap = map
|
||||
} catch (e) {
|
||||
uni.showToast({ title: e.message || '加载城市失败', icon: 'none' })
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
this.$emit('change', payload)
|
||||
},
|
||||
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)
|
||||
},
|
||||
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>
|
||||
.picker-mask{position:fixed;inset:0;background:rgba(15,23,42,.45);z-index:1000;display:flex;align-items:flex-end}.picker-sheet{width:100%;background:#fff;border-radius:24rpx 24rpx 0 0;padding:20rpx;box-sizing:border-box}.picker-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:16rpx}.picker-title{font-size:30rpx;font-weight:800;color:#111827}.picker-close{font-size:26rpx;color:#1677ff}.city-path{padding:12rpx 16rpx;margin-bottom:12rpx;border-radius:16rpx;background:#f8fafc;color:#334155;font-size:24rpx}.city-wheel{height:520rpx;display:flex}.wheel-item{display:flex;align-items:center;justify-content:center;height:88rpx}.wheel-value{font-size:28rpx;font-weight:600;color:#111827}.picker-footer{display:flex;gap:12rpx;margin-top:16rpx}.btn{flex:1;border-radius:16rpx}.btn.ghost{background:#f3f4f6;color:#334155}.btn.primary{background:linear-gradient(135deg,#1677ff,#4f9dff);color:#fff}
|
||||
</style>
|
||||
.picker-mask{position:fixed;inset:0;background:rgba(15,23,42,.45);z-index:1000;display:flex;align-items:flex-end}.picker-sheet{width:100%;background:#fff;border-radius:24rpx 24rpx 0 0;padding:20rpx;box-sizing:border-box}.picker-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:16rpx}.picker-title{font-size:30rpx;font-weight:800;color:#111827}.picker-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}.picker-footer{display:flex;gap:12rpx;margin-top:16rpx}.btn{flex:1;border-radius:16rpx}.btn.ghost{background:#f3f4f6;color:#334155}.btn.primary{background:linear-gradient(135deg,#1677ff,#4f9dff);color:#fff}.btn.primary[disabled]{background:#9cc2ff;color:#fff}
|
||||
</style>
|
||||
|
||||
@@ -1,40 +1,167 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<scroll-view class="scroll" scroll-y>
|
||||
<view class="card" v-for="section in sections" :key="section.key">
|
||||
<view class="scroll">
|
||||
<view
|
||||
v-for="section in sections"
|
||||
:key="section.key"
|
||||
class="card"
|
||||
>
|
||||
<view class="card-title">{{ section.title }}</view>
|
||||
<view v-for="field in section.fields" :key="field.key" class="field">
|
||||
<text class="label">{{ field.label }}<text v-if="field.required" class="req">*</text></text>
|
||||
<template v-if="field.type === 'select'"><picker mode="selector" :range="field.options" @change="e => onSelect(field.key, field.options, e)"><view class="input-like clickable">{{ form[field.key] || field.placeholder }}</view></picker></template>
|
||||
<template v-else-if="field.type === 'datetime'"><view class="input-like clickable" @tap="openDateTime(field.key)">{{ formatDateTime(field.key) || field.placeholder }}</view></template>
|
||||
<template v-else-if="field.type === 'city'"><view class="input-like clickable" @tap="openCity(field.key)">{{ form[field.key] || field.placeholder }}</view></template>
|
||||
<template v-else-if="field.type === 'computed'"><view class="computed-box">{{ computedFieldText(field.key) || field.placeholder || '—' }}</view></template>
|
||||
|
||||
<view
|
||||
v-for="field in section.fields"
|
||||
:key="field.key"
|
||||
class="field"
|
||||
>
|
||||
<text class="label">
|
||||
{{ field.label }}
|
||||
<text v-if="field.required" class="req">*</text>
|
||||
</text>
|
||||
|
||||
<template v-if="field.type === 'select'">
|
||||
<picker
|
||||
mode="selector"
|
||||
:range="field.options"
|
||||
@change="onSelect(field, $event)"
|
||||
>
|
||||
<view class="input-like clickable">
|
||||
{{ getFieldDisplayValue(field) }}
|
||||
</view>
|
||||
</picker>
|
||||
</template>
|
||||
|
||||
<template v-else-if="field.type === 'datetime'">
|
||||
<view class="input-like clickable" @tap="openDateTime(field.key)">
|
||||
{{ getFieldDisplayValue(field) }}
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<template v-else-if="field.type === 'city'">
|
||||
<view class="input-like clickable" @tap="openCity(field.key)">
|
||||
{{ getFieldDisplayValue(field) || field.placeholder || '选择城市' }}
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<template v-else-if="field.type === 'computed'">
|
||||
<view class="computed-box">
|
||||
{{ getFieldDisplayValue(field) }}
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<template v-else-if="field.type === 'file'">
|
||||
<view class="file-picker-wrap">
|
||||
<view class="upload-box clickable" @tap="openFilePicker(field.key)">
|
||||
<view class="upload-box clickable" :class="{ disabled: uploadingFieldKey === field.key }" @tap="openFilePicker(field.key)">
|
||||
<view class="upload-hint">{{ field.placeholder || '点击上传文件' }}</view>
|
||||
<view class="upload-sub">支持文件上传,点击后选择文件</view>
|
||||
<view class="upload-sub">{{ uploadingFieldKey === field.key ? '上传中,请稍候...' : (field.uploadTip || '支持文件上传,点击后选择文件') }}</view>
|
||||
</view>
|
||||
<view class="file-list" v-if="fileFields[field.key] && fileFields[field.key].length">
|
||||
<view class="file-item" v-for="(file, idx) in fileFields[field.key]" :key="file.ossId || file.url || idx">
|
||||
|
||||
<view v-if="field.accept" class="upload-rule">
|
||||
仅支持 {{ field.accept }} 格式文件
|
||||
</view>
|
||||
|
||||
<view v-if="uploadingFieldKey === field.key" class="upload-loading">
|
||||
<text class="loading-dot"></text>
|
||||
<text class="loading-text">文件正在上传中...</text>
|
||||
</view>
|
||||
|
||||
<view
|
||||
v-if="getFileList(field.key).length"
|
||||
class="file-list"
|
||||
>
|
||||
<view
|
||||
v-for="(file, idx) in getFileList(field.key)"
|
||||
:key="file.ossId || file.url || idx"
|
||||
class="file-item"
|
||||
>
|
||||
<text class="file-name">{{ file.originalName || file.fileName || file.url }}</text>
|
||||
<text class="remove" @tap="removeFile(field.key, idx)">删除</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<template v-else-if="field.type === 'textarea'"><textarea class="textarea" v-model="form[field.key]" :maxlength="field.maxlength || 200" :placeholder="field.placeholder" /></template>
|
||||
<template v-else><input class="input" :type="field.inputType || 'text'" v-model="form[field.key]" :placeholder="field.placeholder" /></template>
|
||||
|
||||
<template v-else-if="field.type === 'textarea'">
|
||||
<textarea
|
||||
v-model="form[field.key]"
|
||||
class="textarea"
|
||||
:maxlength="field.maxlength || 200"
|
||||
:placeholder="field.placeholder"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template v-else-if="field.type === 'input' || field.type === 'number' || field.type === 'text' || !field.type">
|
||||
<view class="text-input-wrap">
|
||||
<input
|
||||
v-model="form[field.key]"
|
||||
class="input"
|
||||
:type="normalizeInputType(field.inputType)"
|
||||
:placeholder="field.placeholder"
|
||||
:confirm-type="field.confirmType || 'done'"
|
||||
:adjust-position="true"
|
||||
@focus="onInputFocus(field.key)"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
<template v-else>
|
||||
<input
|
||||
:value="form[field.key]"
|
||||
class="input"
|
||||
type="text"
|
||||
:placeholder="field.placeholder"
|
||||
:confirm-type="field.confirmType || 'done'"
|
||||
:adjust-position="true"
|
||||
@input="onInput(field.key, $event)"
|
||||
@focus="onInputFocus(field.key)"
|
||||
/>
|
||||
</template>
|
||||
</view>
|
||||
</view>
|
||||
<view class="card"><view class="card-title">审批方式</view><view class="field"><text class="label">审批人<text class="req">*</text></text><view class="input-like clickable" @tap="openEmployeePicker('assignee')">{{ assigneeLabel || '点击选择审批人' }}</view></view></view>
|
||||
<view class="card"><view class="card-title">抄送</view><view class="field"><text class="label">抄送人</text><view class="input-like clickable" @tap="openEmployeePicker('cc')">{{ ccUsers.length ? `已选择 ${ccUsers.length} 人` : '点击选择抄送人' }}</view></view></view>
|
||||
<view class="card"><view class="card-title">提交</view><button class="btn primary" :loading="submitting" @tap="submit">提交申请</button></view>
|
||||
</scroll-view>
|
||||
|
||||
<view class="card">
|
||||
<view class="card-title">审批方式</view>
|
||||
<view class="field">
|
||||
<text class="label">审批人<text class="req">*</text></text>
|
||||
<view class="input-like clickable" @tap="openEmployeePicker('assignee')">
|
||||
{{ assigneeLabel || '点击选择审批人' }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="card">
|
||||
<view class="card-title">抄送</view>
|
||||
<view class="field">
|
||||
<text class="label">抄送人</text>
|
||||
<view class="input-like clickable" @tap="openEmployeePicker('cc')">
|
||||
{{ ccLabel }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="card">
|
||||
<view class="card-title">提交</view>
|
||||
<button class="btn primary" :loading="submitting" @tap="submit">提交申请</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<x-file-picker ref="globalFilePicker" @select="onGlobalFileSelect" />
|
||||
<employee-picker :visible.sync="employeeSheetVisible" :title="employeeMode === 'assignee' ? '选择审批人' : '选择抄送人'" :list="employees" :multiple="employeeMode === 'cc'" :value="employeeMode === 'assignee' ? assignee : ccUsers" @change="onEmployeeChange" />
|
||||
<date-time-picker :visible.sync="dateTimeSheetVisible" :value="form[datetimeFieldKey]" @change="onDateTimeChange" />
|
||||
<city-picker :visible.sync="citySheetVisible" :value="form[cityFieldKey]" @change="onCityChange" />
|
||||
<employee-picker
|
||||
:visible.sync="employeeSheetVisible"
|
||||
:title="employeeMode === 'assignee' ? '选择审批人' : '选择抄送人'"
|
||||
:list="employees"
|
||||
:multiple="employeeMode === 'cc'"
|
||||
:value="employeeMode === 'assignee' ? assignee : ccUsers"
|
||||
@change="onEmployeeChange"
|
||||
/>
|
||||
<date-time-picker
|
||||
:visible.sync="dateTimeSheetVisible"
|
||||
:value="form[datetimeFieldKey]"
|
||||
@change="onDateTimeChange"
|
||||
/>
|
||||
<city-picker
|
||||
:visible.sync="citySheetVisible"
|
||||
:value="form[cityFieldKey]"
|
||||
@change="onCityChange"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
@@ -50,117 +177,328 @@ import XFilePicker from '@/components/x-native-uploader/x-file-picker.vue'
|
||||
|
||||
export default {
|
||||
components: { EmployeePicker, DateTimePicker, CityPicker, XFilePicker },
|
||||
props: { title: String, subtitle: String, bizType: String, requestApi: Function, initialForm: Object, sections: Array, flowFields: { type: Array, default: () => [] } },
|
||||
data() { return { submitting: false, form: JSON.parse(JSON.stringify(this.initialForm || {})), employees: [], employeeSheetVisible: false, employeeMode: 'assignee', assignee: null, ccUsers: [], cachedAssigneeKey: '', dateTimeSheetVisible: false, datetimeFieldKey: '', citySheetVisible: false, cityFieldKey: '', imageFields: {}, fileFields: {}, currentEmpId: null } },
|
||||
computed: { assigneeLabel() { return this.assignee ? this.employeeName(this.assignee) : '' } },
|
||||
async created() {
|
||||
this.cachedAssigneeKey = `hrm_manual_assignee_${this.bizType || 'default'}`;
|
||||
await this.loadEmployees();
|
||||
await this.loadCurrentEmpId()
|
||||
this.restoreAssignee()
|
||||
props: {
|
||||
title: String,
|
||||
subtitle: String,
|
||||
bizType: String,
|
||||
requestApi: Function,
|
||||
initialForm: Object,
|
||||
sections: Array,
|
||||
flowFields: { type: Array, default: () => [] }
|
||||
},
|
||||
watch: {
|
||||
'form.startTime'(val) { this.autoFillHours() },
|
||||
'form.endTime'(val) { this.autoFillHours() }
|
||||
data() {
|
||||
return {
|
||||
submitting: false,
|
||||
form: JSON.parse(JSON.stringify(this.initialForm || {})),
|
||||
employees: [],
|
||||
employeeSheetVisible: false,
|
||||
employeeMode: 'assignee',
|
||||
assignee: null,
|
||||
ccUsers: [],
|
||||
cachedAssigneeKey: '',
|
||||
dateTimeSheetVisible: false,
|
||||
datetimeFieldKey: '',
|
||||
citySheetVisible: false,
|
||||
cityFieldKey: '',
|
||||
fileFields: {},
|
||||
currentFileFieldKey: '',
|
||||
currentFileFieldConfig: null,
|
||||
uploadingFieldKey: '',
|
||||
currentInputFieldKey: '',
|
||||
currentEmpId: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
assigneeLabel() {
|
||||
return this.assignee ? this.employeeName(this.assignee) : ''
|
||||
},
|
||||
ccLabel() {
|
||||
return this.ccUsers.length ? '已选择 ' + this.ccUsers.length + ' 人' : '点击选择抄送人'
|
||||
}
|
||||
},
|
||||
async created() {
|
||||
this.cachedAssigneeKey = 'hrm_manual_assignee_' + (this.bizType || 'default')
|
||||
await this.loadEmployees()
|
||||
await this.loadCurrentEmpId()
|
||||
this.restoreAssignee()
|
||||
},
|
||||
watch: {
|
||||
'form.startTime'() {
|
||||
this.autoFillHours()
|
||||
},
|
||||
'form.endTime'() {
|
||||
this.autoFillHours()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onSelect(fieldKey, options, e) {
|
||||
const idx = Number(e.detail.value)
|
||||
this.$set(this.form, fieldKey, options[idx])
|
||||
getFieldValue(fieldKey) {
|
||||
return this.form[fieldKey]
|
||||
},
|
||||
getFieldDisplayValue(field) {
|
||||
if (field.type === 'datetime') return this.formatDateTime(field.key) || field.placeholder || ''
|
||||
if (field.type === 'computed') return this.computedFieldText(field.key) || field.placeholder || '—'
|
||||
var value = this.getFieldValue(field.key)
|
||||
return value !== undefined && value !== null && value !== '' ? value : (field.placeholder || '')
|
||||
},
|
||||
getFileList(fieldKey) {
|
||||
return this.fileFields[fieldKey] || []
|
||||
},
|
||||
normalizeInputType(inputType) {
|
||||
return inputType || 'text'
|
||||
},
|
||||
onInputFocus(fieldKey) {
|
||||
this.currentInputFieldKey = fieldKey
|
||||
},
|
||||
onInput(fieldKey, e) {
|
||||
var value = e && e.detail ? e.detail.value : e
|
||||
this.$set(this.form, fieldKey, value)
|
||||
},
|
||||
|
||||
onSelect(field, e) {
|
||||
var idx = Number(e.detail.value)
|
||||
var options = field.options || []
|
||||
this.$set(this.form, field.key, options[idx])
|
||||
},
|
||||
employeeName(emp) {
|
||||
return emp && (emp.empName || emp.nickName || emp.userName || emp.realName) || ('员工' + ((emp && (emp.userId || emp.id)) || ''))
|
||||
},
|
||||
employeeDept(emp) {
|
||||
return (emp && emp.dept && emp.dept.deptName) || (emp && emp.deptName) || '未分配部门'
|
||||
},
|
||||
async loadEmployees() {
|
||||
try {
|
||||
var res = await listUser({ pageNum: 1, pageSize: 500 })
|
||||
this.employees = res.rows || res.data || []
|
||||
} catch (e) {
|
||||
this.employees = []
|
||||
}
|
||||
},
|
||||
async loadCurrentEmpId() {
|
||||
try {
|
||||
var oaId = uni.getStorageSync('oaId')
|
||||
if (!oaId) return
|
||||
var emp = await getEmployeeByUserId(oaId)
|
||||
if (emp && emp.data && emp.data.empId) {
|
||||
this.currentEmpId = emp.data.empId
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('[loadCurrentEmpId] error', e)
|
||||
}
|
||||
},
|
||||
restoreAssignee() {
|
||||
try {
|
||||
var raw = uni.getStorageSync(this.cachedAssigneeKey)
|
||||
if (!raw) return
|
||||
var cached = typeof raw === 'string' ? JSON.parse(raw) : raw
|
||||
var id = String(cached.userId || cached.empId || '')
|
||||
var hit = this.employees.find(function (e) {
|
||||
return String(e.userId || e.id) === id
|
||||
})
|
||||
if (hit) this.assignee = hit
|
||||
} catch (e) {}
|
||||
},
|
||||
openEmployeePicker(mode) {
|
||||
this.employeeMode = mode
|
||||
this.employeeSheetVisible = true
|
||||
},
|
||||
onEmployeeChange(val) {
|
||||
if (this.employeeMode === 'assignee') {
|
||||
this.assignee = val
|
||||
try {
|
||||
uni.setStorageSync(this.cachedAssigneeKey, JSON.stringify({
|
||||
userId: val && (val.userId || val.id),
|
||||
empId: val && val.empId || '',
|
||||
empName: this.employeeName(val),
|
||||
deptName: this.employeeDept(val)
|
||||
}))
|
||||
} catch (e) {}
|
||||
} else {
|
||||
this.ccUsers = val || []
|
||||
}
|
||||
},
|
||||
employeeName(emp) { return emp?.empName || emp?.nickName || emp?.userName || emp?.realName || `员工${emp?.userId || emp?.id || ''}` },
|
||||
employeeDept(emp) { return emp?.dept?.deptName || emp?.deptName || '未分配部门' },
|
||||
async loadEmployees() { try { const res = await listUser({ pageNum: 1, pageSize: 500 }); this.employees = res.rows || res.data || [] } catch (e) { this.employees = [] } },
|
||||
async loadCurrentEmpId() { try { const oaId = uni.getStorageSync('oaId'); if (!oaId) return; const emp = await getEmployeeByUserId(oaId); if (emp?.data?.empId) this.currentEmpId = emp.data.empId } catch (e) { console.log('[loadCurrentEmpId] error', e) } },
|
||||
restoreAssignee() { try { const raw = uni.getStorageSync(this.cachedAssigneeKey); if (!raw) return; const cached = typeof raw === 'string' ? JSON.parse(raw) : raw; const id = String(cached.userId || cached.empId || ''); const hit = this.employees.find(e => String(e.userId || e.id) === id); if (hit) this.assignee = hit } catch (e) {} },
|
||||
openEmployeePicker(mode) { this.employeeMode = mode; this.employeeSheetVisible = true },
|
||||
onEmployeeChange(val) { if (this.employeeMode === 'assignee') { this.assignee = val; try { uni.setStorageSync(this.cachedAssigneeKey, JSON.stringify({ userId: val.userId || val.id, empId: val.empId || '', empName: this.employeeName(val), deptName: this.employeeDept(val) })) } catch (e) {} } else { this.ccUsers = val || [] } },
|
||||
openFilePicker(fieldKey) {
|
||||
this.currentFileFieldKey = fieldKey
|
||||
console.log('[RequestForm] openFilePicker', fieldKey, this.$refs.globalFilePicker)
|
||||
const picker = this.$refs.globalFilePicker
|
||||
this.currentFileFieldConfig = this.flowFields.find(function (f) { return f.key === fieldKey }) || null
|
||||
var picker = this.$refs.globalFilePicker
|
||||
if (picker && picker.open) {
|
||||
picker.open()
|
||||
} else {
|
||||
console.warn('[RequestForm] file picker ref missing or open() unavailable,', fieldKey, picker)
|
||||
console.warn('[RequestForm] file picker ref missing or open() unavailable', fieldKey)
|
||||
}
|
||||
},
|
||||
async onGlobalFileSelect(payload) {
|
||||
const fieldKey = this.currentFileFieldKey
|
||||
console.log('[RequestForm] file select', fieldKey, payload)
|
||||
var fieldKey = this.currentFileFieldKey
|
||||
if (!fieldKey) return
|
||||
const files = Array.isArray(payload) ? payload : [payload]
|
||||
var files = Array.isArray(payload) ? payload : [payload]
|
||||
if (!this.fileFields[fieldKey]) this.$set(this.fileFields, fieldKey, [])
|
||||
for (const f of files.filter(Boolean)) {
|
||||
try {
|
||||
const uploaded = await uploadFile({
|
||||
path: f.path || f.url || '',
|
||||
name: f.name || f.fileName || 'file',
|
||||
size: f.size || 0,
|
||||
type: f.type || ''
|
||||
})
|
||||
this.fileFields[fieldKey].push({
|
||||
name: uploaded.fileName || f.name || f.fileName || 'file',
|
||||
extname: (f.name || f.fileName || '').split('.').pop() || '',
|
||||
url: uploaded.url || f.path || f.url || '',
|
||||
ossId: uploaded.ossId || uploaded.url || f.path || f.url || '',
|
||||
fileName: uploaded.fileName || f.fileName || f.name || '',
|
||||
originalName: f.name || f.fileName || uploaded.fileName || ''
|
||||
})
|
||||
} catch (e) {
|
||||
uni.showToast({ title: e.message || '上传失败', icon: 'none' })
|
||||
var cfg = this.currentFileFieldConfig || {}
|
||||
var allowedExt = (cfg.accept || '').toLowerCase().split(',').map(function (x) { return x.trim() }).filter(Boolean)
|
||||
if (cfg.key === 'applyFileIds' && this.bizType === 'seal' && allowedExt.indexOf('.pdf') === -1) {
|
||||
allowedExt = ['.pdf']
|
||||
}
|
||||
|
||||
this.uploadingFieldKey = fieldKey
|
||||
uni.showLoading({ title: '上传中...' })
|
||||
try {
|
||||
for (var i = 0; i < files.length; i++) {
|
||||
var f = files[i]
|
||||
if (!f) continue
|
||||
var name = String(f.name || f.fileName || '').toLowerCase()
|
||||
var ext = name.indexOf('.') >= 0 ? '.' + name.split('.').pop() : ''
|
||||
if (allowedExt.length && allowedExt.indexOf(ext) === -1) {
|
||||
uni.showToast({ title: '仅支持上传 PDF 文件', icon: 'none' })
|
||||
continue
|
||||
}
|
||||
try {
|
||||
var uploaded = await uploadFile({
|
||||
path: f.path || f.url || '',
|
||||
name: f.name || f.fileName || 'file',
|
||||
size: f.size || 0,
|
||||
type: f.type || ''
|
||||
})
|
||||
this.fileFields[fieldKey].push({
|
||||
name: uploaded.fileName || f.name || f.fileName || 'file',
|
||||
extname: ext.replace('.', '') || (f.name || f.fileName || '').split('.').pop() || '',
|
||||
url: uploaded.url || f.path || f.url || '',
|
||||
ossId: uploaded.ossId || uploaded.url || f.path || f.url || '',
|
||||
fileName: uploaded.fileName || f.fileName || f.name || '',
|
||||
originalName: f.name || f.fileName || uploaded.fileName || ''
|
||||
})
|
||||
} catch (e) {
|
||||
uni.showToast({ title: e.message || '上传失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
this.syncFileIds(fieldKey)
|
||||
} finally {
|
||||
uni.hideLoading()
|
||||
this.uploadingFieldKey = ''
|
||||
this.currentFileFieldKey = ''
|
||||
this.currentFileFieldConfig = null
|
||||
}
|
||||
},
|
||||
removeFile(fieldKey, idx) {
|
||||
var arr = (this.fileFields[fieldKey] || []).slice()
|
||||
arr.splice(idx, 1)
|
||||
this.$set(this.fileFields, fieldKey, arr)
|
||||
this.syncFileIds(fieldKey)
|
||||
},
|
||||
syncFileIds(fieldKey) {
|
||||
var arr = this.fileFields[fieldKey] || []
|
||||
this.$set(this.form, fieldKey, arr.map(function (x) { return x.ossId }).join(','))
|
||||
},
|
||||
openDateTime(fieldKey) {
|
||||
this.datetimeFieldKey = fieldKey
|
||||
this.dateTimeSheetVisible = true
|
||||
},
|
||||
formatDateTime(key) {
|
||||
var v = this.form[key]
|
||||
if (!v) return ''
|
||||
return String(v).replace('T', ' ').substring(0, 16)
|
||||
},
|
||||
onDateTimeChange(val) {
|
||||
if (!this.datetimeFieldKey) return
|
||||
this.$set(this.form, this.datetimeFieldKey, val)
|
||||
this.autoFillHours()
|
||||
},
|
||||
computedFieldText(key) {
|
||||
if (key === 'hours') {
|
||||
var val = this.form[key]
|
||||
return val ? val + ' 小时' : ''
|
||||
}
|
||||
var value = this.form[key]
|
||||
return value !== undefined && value !== null ? value : ''
|
||||
},
|
||||
openCity(fieldKey) {
|
||||
this.cityFieldKey = fieldKey
|
||||
this.citySheetVisible = true
|
||||
},
|
||||
onCityChange(val) {
|
||||
if (!this.cityFieldKey) return
|
||||
this.$set(this.form, this.cityFieldKey, val)
|
||||
},
|
||||
autoFillHours() {
|
||||
var s = this.parseDT(this.form.startTime)
|
||||
var e = this.parseDT(this.form.endTime)
|
||||
if (!s || !e) return
|
||||
var ms = e.getTime() - s.getTime()
|
||||
if (ms <= 0) return
|
||||
var hours = (Math.round((ms / 3600000) * 2) / 2).toFixed(1).replace(/\.0$/, '')
|
||||
this.$set(this.form, 'hours', hours)
|
||||
},
|
||||
parseDT(v) {
|
||||
if (!v) return null
|
||||
var d = new Date(String(v).replace('T', ' ').replace(/-/g, '/'))
|
||||
return Number.isNaN(d.getTime()) ? null : d
|
||||
},
|
||||
validateFlowFields() {
|
||||
for (var i = 0; i < this.flowFields.length; i++) {
|
||||
var f = this.flowFields[i]
|
||||
if (f.required && !this.form[f.key]) {
|
||||
uni.showToast({ title: '请填写' + f.label, icon: 'none' })
|
||||
return false
|
||||
}
|
||||
}
|
||||
this.$set(this.form, fieldKey, this.fileFields[fieldKey].map(x => x.ossId).join(','))
|
||||
this.currentFileFieldKey = ''
|
||||
if (!this.assignee) {
|
||||
uni.showToast({ title: '请选择审批人', icon: 'none' })
|
||||
return false
|
||||
}
|
||||
return true
|
||||
},
|
||||
removeFile(fieldKey, idx) { const arr = (this.fileFields[fieldKey] || []).slice(); arr.splice(idx, 1); this.$set(this.fileFields, fieldKey, arr); this.$set(this.form, fieldKey, arr.map(x => x.ossId).join(',')) },
|
||||
openDateTime(fieldKey) { this.datetimeFieldKey = fieldKey; this.dateTimeSheetVisible = true },
|
||||
formatDateTime(key) { const v = this.form[key]; if (!v) return ''; return v.replace('T', ' ').substring(0, 16) },
|
||||
onDateTimeChange(val) { if (!this.datetimeFieldKey) return; this.$set(this.form, this.datetimeFieldKey, val); this.autoFillHours() },
|
||||
computedFieldText(key) { if (key === 'hours') { const val = this.form[key]; return val ? `${val} 小时` : '' } return this.form[key] || '' },
|
||||
openCity(fieldKey) { this.cityFieldKey = fieldKey; this.citySheetVisible = true },
|
||||
onCityChange(val) { if (!this.cityFieldKey) return; this.$set(this.form, this.cityFieldKey, val) },
|
||||
onFileSelect(fieldKey) { if (!this.fileFields[fieldKey]) this.$set(this.fileFields, fieldKey, []) },
|
||||
onFileSuccess(fieldKey, e) {
|
||||
if (!this.fileFields[fieldKey]) this.$set(this.fileFields, fieldKey, [])
|
||||
const files = e?.tempFiles || []
|
||||
const current = Array.isArray(this.fileFields[fieldKey]) ? this.fileFields[fieldKey] : []
|
||||
this.fileFields[fieldKey] = [...current, ...files.map(f => ({
|
||||
name: f.name || f.fileName || 'file',
|
||||
extname: (f.name || f.fileName || '').split('.').pop() || '',
|
||||
url: f.url || f.tempFilePath || f.path || '',
|
||||
ossId: f.ossId || f.fileID || f.url || '',
|
||||
fileName: f.fileName || f.name || '',
|
||||
originalName: f.originalName || f.name || f.fileName || ''
|
||||
}))]
|
||||
this.$set(this.form, fieldKey, this.fileFields[fieldKey].map(x => x.ossId).join(','))
|
||||
submit() {
|
||||
this.autoFillHours()
|
||||
if (!this.validateFlowFields()) return
|
||||
this.doSubmit()
|
||||
},
|
||||
onFileDelete(fieldKey, e) {
|
||||
const fileList = Array.isArray(this.fileFields[fieldKey]) ? this.fileFields[fieldKey] : []
|
||||
const index = e?.index ?? e?.indexs?.[0] ?? -1
|
||||
if (index >= 0) fileList.splice(index, 1)
|
||||
this.$set(this.fileFields, fieldKey, [...fileList])
|
||||
this.$set(this.form, fieldKey, fileList.map(x => x.ossId).join(','))
|
||||
buildSubmitPayload() {
|
||||
return {
|
||||
...this.form,
|
||||
status: 'pending',
|
||||
bizType: this.bizType,
|
||||
manualAssigneeUserId: this.assignee.userId || this.assignee.id,
|
||||
tplId: this.form.tplId || null,
|
||||
empId: this.currentEmpId
|
||||
}
|
||||
},
|
||||
pickImages() {},
|
||||
async doSubmit() {
|
||||
this.submitting = true
|
||||
try {
|
||||
var payload = this.buildSubmitPayload()
|
||||
console.log('[RequestForm] submit payload', payload)
|
||||
var res = await this.requestApi(payload)
|
||||
var inst = res && (res.data || res)
|
||||
|
||||
if (!inst || (!inst.instId && !inst.bizId)) {
|
||||
console.warn('[RequestForm] submit success but no flow instance returned', res)
|
||||
}
|
||||
|
||||
autoFillHours() {
|
||||
const s = this.parseDT(this.form.startTime), e = this.parseDT(this.form.endTime)
|
||||
if (!s || !e) return;
|
||||
const ms = e.getTime() - s.getTime();
|
||||
if (ms <= 0) return;
|
||||
const hours = (Math.round((ms / 3600000) * 2) / 2).toFixed(1).replace(/\.0$/, '');
|
||||
this.$set(this.form, 'hours', hours)
|
||||
},
|
||||
parseDT(v) { if (!v) return null; const d = new Date(String(v).replace('T', ' ').replace(/-/g, '/')); return Number.isNaN(d.getTime()) ? null : d },
|
||||
submit() { this.autoFillHours(); for (const f of this.flowFields) if (f.required && !this.form[f.key]) return uni.showToast({ title: `请填写${f.label}`, icon: 'none' }); if (!this.assignee) return uni.showToast({ title: '请选择审批人', icon: 'none' }); this.doSubmit() },
|
||||
async doSubmit() { this.submitting = true; try { const payload = { ...this.form, status: 'pending', empId: this.currentEmpId, manualAssigneeUserId: this.assignee.userId || this.assignee.id }; const res = await this.requestApi(payload); const inst = res?.data || res; if (this.ccUsers.length && inst?.instId) await ccFlowTask({ instId: inst.instId, bizId: inst.bizId, bizType: this.bizType, ccUserIds: this.ccUsers.map(u => u.userId || u.id), remark: '手机端抄送', fromUserId: this.$store?.state?.user?.id || '', nodeId: 0, nodeName: '节点#0', readFlag: 0 }); uni.showToast({ title: '提交成功', icon: 'success' }); setTimeout(() => uni.navigateBack({ delta: 1 }), 500) } catch (e) { uni.showToast({ title: e.message || '提交失败', icon: 'none' }) } finally { this.submitting = false } }
|
||||
if (this.ccUsers.length && inst && inst.instId) {
|
||||
await ccFlowTask({
|
||||
instId: inst.instId,
|
||||
bizId: inst.bizId,
|
||||
bizType: this.bizType,
|
||||
ccUserIds: this.ccUsers.map(function (u) { return u.userId || u.id }),
|
||||
remark: '手机端抄送',
|
||||
fromUserId: (this.$store && this.$store.state && this.$store.state.user && this.$store.state.user.id) || '',
|
||||
nodeId: 0,
|
||||
nodeName: '节点#0',
|
||||
readFlag: 0
|
||||
})
|
||||
}
|
||||
|
||||
uni.showToast({ title: '提交成功', icon: 'success' })
|
||||
setTimeout(function () {
|
||||
uni.navigateBack({ delta: 1 })
|
||||
}, 500)
|
||||
} catch (e) {
|
||||
uni.showToast({ title: e.message || '提交失败', icon: 'none' })
|
||||
} finally {
|
||||
this.submitting = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page{min-height:100vh;background:#f5f7fb}.scroll{height:100vh;padding:24rpx;box-sizing:border-box}.card{background:#fff;border-radius:20rpx;padding:24rpx;margin-bottom:20rpx}.card-title{font-size:30rpx;font-weight:700;color:#111827;margin-bottom:16rpx}.field{margin-bottom:18rpx}.label{display:block;margin-bottom:8rpx;font-size:24rpx;color:#6b7280}.req{color:#ef4444;margin-left:4rpx}.input-like,.input,.textarea{width:100%;box-sizing:border-box;border:1rpx solid #e5e7eb;border-radius:16rpx;background:#f9fafb;padding:20rpx;font-size:28rpx;color:#111827}.clickable{color:#111827}.textarea{min-height:160rpx}.computed-box{width:100%;box-sizing:border-box;padding:18rpx 20rpx;border-radius:16rpx;background:#eef6ff;color:#6b8fd6;font-size:26rpx;font-weight:500;line-height:1.5}.upload-box{border:1rpx dashed #cbd5e1;border-radius:18rpx;background:linear-gradient(180deg,#fbfdff 0%,#f4f8ff 100%);padding:24rpx 22rpx;box-shadow:0 8rpx 24rpx rgba(22,119,255,.06);}.upload-hint{font-size:28rpx;color:#111827;font-weight:700}.upload-sub{margin-top:8rpx;font-size:22rpx;color:#94a3b8;line-height:1.5}.file-list{margin-top:16rpx;display:flex;flex-direction:column;gap:12rpx}.file-item{display:flex;align-items:center;justify-content:space-between;padding:18rpx 18rpx;border-radius:16rpx;background:#f8fafc;border:1rpx solid #e5eefc}.file-name{flex:1;font-size:26rpx;color:#334155;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;margin-right:16rpx}.remove{font-size:24rpx;color:#ef4444;flex-shrink:0}.btn.primary{width:100%;background:#1677ff;color:#fff;border-radius:16rpx}
|
||||
</style>
|
||||
.page{min-height:100vh;background:#f5f7fb}.scroll{height:100vh;padding:24rpx;box-sizing:border-box}.card{background:#fff;border-radius:20rpx;padding:24rpx;margin-bottom:20rpx}.card-title{font-size:30rpx;font-weight:700;color:#111827;margin-bottom:16rpx}.field{margin-bottom:18rpx}.label{display:block;margin-bottom:8rpx;font-size:24rpx;color:#6b7280}.req{color:#ef4444;margin-left:4rpx}.input-like,.input,.textarea{width:100%;box-sizing:border-box;border:1rpx solid #e5e7eb;border-radius:16rpx;background:#f9fafb;padding:20rpx;font-size:28rpx;color:#111827}.text-input-wrap{position:relative;z-index:20;background:transparent;pointer-events:auto}.text-input-wrap .input{position:relative;z-index:21;pointer-events:auto;touch-action:manipulation}.clickable{color:#111827}.textarea{min-height:160rpx}.computed-box{width:100%;box-sizing:border-box;padding:18rpx 20rpx;border-radius:16rpx;background:#eef6ff;color:#6b8fd6;font-size:26rpx;font-weight:500;line-height:1.5}.upload-box{position:relative;z-index:1;border:1rpx dashed #cbd5e1;border-radius:18rpx;background:linear-gradient(180deg,#fbfdff 0%,#f4f8ff 100%);padding:24rpx 22rpx;box-shadow:0 8rpx 24rpx rgba(22,119,255,.06);pointer-events:auto}.upload-hint{font-size:28rpx;color:#111827;font-weight:700}.upload-sub{margin-top:8rpx;font-size:22rpx;color:#94a3b8;line-height:1.5}.upload-rule{margin-top:8rpx;font-size:22rpx;color:#f59e0b;line-height:1.5}.upload-loading{margin-top:12rpx;display:flex;align-items:center;gap:12rpx;padding:16rpx 18rpx;border-radius:16rpx;background:#f0f7ff;color:#1677ff;font-size:24rpx;font-weight:500}.loading-dot{width:16rpx;height:16rpx;border-radius:50%;background:#1677ff;box-shadow:0 0 0 0 rgba(22,119,255,.45);animation:uploadPulse 1.2s infinite}.loading-text{flex:1}.file-list{margin-top:16rpx;display:flex;flex-direction:column;gap:12rpx}.file-item{display:flex;align-items:center;justify-content:space-between;padding:18rpx 18rpx;border-radius:16rpx;background:#f8fafc;border:1rpx solid #e5eefc}.file-name{flex:1;font-size:26rpx;color:#334155;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;margin-right:16rpx}.remove{font-size:24rpx;color:#ef4444;flex-shrink:0}.btn.primary{width:100%;background:#1677ff;color:#fff;border-radius:16rpx}@keyframes uploadPulse{0%{transform:scale(.85);opacity:.7}50%{transform:scale(1);opacity:1}100%{transform:scale(.85);opacity:.7}}
|
||||
</style>
|
||||
|
||||
@@ -1,81 +1,84 @@
|
||||
<template>
|
||||
<!-- 出差申请详情容器,复用统一卡片样式 -->
|
||||
<view class="reimburse-detail-container">
|
||||
<!-- 出差类型 - 带分类图标 -->
|
||||
<view class="leave-detail-container">
|
||||
<view class="detail-item single-item">
|
||||
<text class="item-icon icon-travel-type"></text>
|
||||
<view class="item-label">拨款类型</view>
|
||||
<view class="item-label">拨款类型:</view>
|
||||
<view class="item-value">{{ detail.appropriationType || '无' }}</view>
|
||||
</view>
|
||||
<!-- 预估费用 - 突出显示,带金额图标+主题色+加粗(财务核心字段) -->
|
||||
|
||||
<view class="detail-item single-item amount-item">
|
||||
<text class="item-icon icon-amount"></text>
|
||||
<view class="item-label">拨款总金额</view>
|
||||
<view class="item-label">拨款总金额:</view>
|
||||
<view class="item-value">{{ detail.amount || 0 }} 元</view>
|
||||
</view>
|
||||
<!-- 收款人姓名 - 带用户图标 -->
|
||||
|
||||
<view class="detail-item single-item">
|
||||
<text class="item-icon icon-user"></text>
|
||||
<view class="item-label">收款人姓名</view>
|
||||
<view class="item-label">收款人姓名:</view>
|
||||
<view class="item-value">{{ detail.payeeName || '无' }}</view>
|
||||
</view>
|
||||
<!-- 开户银行 - 带银行专属图标(贴合财务场景) -->
|
||||
|
||||
<view class="detail-item single-item">
|
||||
<text class="item-icon icon-bank"></text>
|
||||
<view class="item-label">开户银行</view>
|
||||
<view class="item-label">开户银行:</view>
|
||||
<view class="item-value">{{ detail.bankName || '无' }}</view>
|
||||
</view>
|
||||
<!-- 银行账号 - 带卡号专属图标 -->
|
||||
|
||||
<view class="detail-item single-item">
|
||||
<text class="item-icon icon-card"></text>
|
||||
<view class="item-label">银行账号</view>
|
||||
<view class="item-label">银行账号:</view>
|
||||
<view class="item-value">{{ detail.bankAccount || '无' }}</view>
|
||||
</view>
|
||||
<!-- 备注 - 带笔记图标,多行文适配 -->
|
||||
|
||||
<view class="detail-item single-item multi-line">
|
||||
<text class="item-icon icon-remark"></text>
|
||||
<view class="item-label">拨款事由</view>
|
||||
<view class="item-label">拨款事由:</view>
|
||||
<view class="item-value">{{ detail.reason || '无' }}</view>
|
||||
</view>
|
||||
<!-- 备注 - 带笔记图标,多行文适配 -->
|
||||
|
||||
<view class="detail-item single-item multi-line">
|
||||
<text class="item-icon icon-remark"></text>
|
||||
<view class="item-label">备注</view>
|
||||
<view class="item-label">备注:</view>
|
||||
<view class="item-value">{{ detail.remark || '无' }}</view>
|
||||
</view>
|
||||
<!-- 创建人 - 带创建用户图标 -->
|
||||
|
||||
<view class="detail-item single-item">
|
||||
<text class="item-icon icon-creator"></text>
|
||||
<view class="item-label">申请人</view>
|
||||
<view class="item-label">申请人:</view>
|
||||
<view class="item-value">{{ detail.createBy || '无' }}</view>
|
||||
</view>
|
||||
|
||||
<view class="detail-item single-item">
|
||||
<text class="item-icon icon-creator"></text>
|
||||
<view class="item-label">申请时间</view>
|
||||
<view class="item-value">{{ detail.createTime || '无' }}</view>
|
||||
<text class="item-icon icon-date"></text>
|
||||
<view class="item-label">申请时间:</view>
|
||||
<view class="item-value">{{ formatTime(detail.createTime) || '无' }}</view>
|
||||
</view>
|
||||
|
||||
|
||||
<view class="detail-item single-item">
|
||||
<text class="item-icon icon-date"></text>
|
||||
<view class="item-label">附件:</view>
|
||||
<view class="item-value">
|
||||
<text class="item-icon icon-date"></text>
|
||||
<view class="item-label">附件:</view>
|
||||
<view class="item-value">
|
||||
<view v-if="uploading" class="upload-loading">
|
||||
<text class="loading-dot"></text>
|
||||
<text class="loading-text">附件正在上传中...</text>
|
||||
</view>
|
||||
<oa-file-list :files="detail.accessoryApplyIds"></oa-file-list>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="detail-item single-item amount-item" @click="openPopup" v-if="detail.projectId">
|
||||
|
||||
<view class="detail-item single-item amount-item" v-if="detail.projectId" @click="openPopup">
|
||||
<text class="item-icon icon-creator"></text>
|
||||
<view class="item-label">项目名称</view>
|
||||
<view class="item-label">项目名称:</view>
|
||||
<view class="item-value">{{ detail.projectName || '无' }}</view>
|
||||
</view>
|
||||
<view class="detail-item single-item amount-item" @click="openPopup" v-if="detail.projectId">
|
||||
<view class="detail-item single-item amount-item" v-if="detail.projectId" @click="openPopup">
|
||||
<text class="item-icon icon-creator"></text>
|
||||
<view class="item-label">项目编号</view>
|
||||
<view class="item-label">项目编号:</view>
|
||||
<view class="item-value">{{ detail.projectNum || '无' }}</view>
|
||||
</view>
|
||||
<view class="detail-item single-item amount-item" @click="openPopup" v-if="detail.projectId">
|
||||
<view class="detail-item single-item amount-item" v-if="detail.projectId" @click="openPopup">
|
||||
<text class="item-icon icon-creator"></text>
|
||||
<view class="item-label">项目代号</view>
|
||||
<view class="item-label">项目代号:</view>
|
||||
<view class="item-value">{{ detail.projectCode || '无' }}</view>
|
||||
</view>
|
||||
|
||||
@@ -119,55 +122,113 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {
|
||||
getAppropriationReq
|
||||
} from '@/api/hrm/appropriation.js'
|
||||
import { getAppropriationReq } from '@/api/hrm/appropriation.js'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
bizId: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
detail: {}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
bizId: {
|
||||
handler(newVal) {
|
||||
// 增加空值判断,避免无效请求
|
||||
if (newVal) {
|
||||
getAppropriationReq(newVal).then(res => {
|
||||
this.detail = res.data || {}
|
||||
console.log(this.detail)
|
||||
})
|
||||
}
|
||||
},
|
||||
immediate: true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* 时间格式化方法
|
||||
* 将 "2026-01-08 00:00:00" 格式化为 "2026-01-08"
|
||||
* @param {String} timeStr - 原始时间字符串
|
||||
* @returns {String} 格式化后的日期
|
||||
*/
|
||||
formatTime(timeStr) {
|
||||
if (!timeStr) return '';
|
||||
const [datePart] = timeStr.split(' ');
|
||||
return datePart;
|
||||
export default {
|
||||
props: {
|
||||
bizId: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
detail: {}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
bizId: {
|
||||
handler(newVal) {
|
||||
if (!newVal) return
|
||||
getAppropriationReq(newVal)
|
||||
.then(res => {
|
||||
this.detail = res.data || {}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('获取拨款详情失败:', err)
|
||||
uni.showToast({ title: '获取详情失败', icon: 'none', duration: 2000 })
|
||||
})
|
||||
},
|
||||
openPopup() {
|
||||
this.$refs.popup.open()
|
||||
}
|
||||
immediate: true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
formatTime(timeStr) {
|
||||
if (!timeStr) return ''
|
||||
const [datePart] = timeStr.split(' ')
|
||||
return datePart
|
||||
},
|
||||
openPopup() {
|
||||
this.$refs.popup.open()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.leave-detail-container {
|
||||
background: #ffffff;
|
||||
border-radius: 18rpx;
|
||||
margin: 20rpx 16rpx;
|
||||
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.05);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
line-height: 48rpx;
|
||||
padding: 20rpx 0;
|
||||
border-bottom: 1px solid #f9f9f9;
|
||||
}
|
||||
.detail-item:last-child { border-bottom: none; }
|
||||
.single-item { flex-direction: row; }
|
||||
.item-icon {
|
||||
display: inline-block;
|
||||
width: 32rpx;
|
||||
height: 32rpx;
|
||||
margin-right: 12rpx;
|
||||
margin-top: 8rpx;
|
||||
flex-shrink: 0;
|
||||
color: #409eff;
|
||||
font-family: "iconfont";
|
||||
}
|
||||
.icon-travel-type { content: "\e645"; }
|
||||
.icon-user { content: "\e6b8"; }
|
||||
.icon-amount { content: "\e62e"; }
|
||||
.icon-bank { content: "\e673"; }
|
||||
.icon-card { content: "\e682"; }
|
||||
.icon-remark { content: "\e634"; }
|
||||
.icon-creator { content: "\e600"; }
|
||||
.icon-date { content: "\e637"; }
|
||||
.item-label {
|
||||
width: 160rpx;
|
||||
font-size: 30rpx;
|
||||
color: #909399;
|
||||
font-weight: 500;
|
||||
flex-shrink: 0;
|
||||
padding-top: 2rpx;
|
||||
}
|
||||
.item-value {
|
||||
flex: 1;
|
||||
font-size: 30rpx;
|
||||
color: #333333;
|
||||
word-break: break-all;
|
||||
line-height: 48rpx;
|
||||
}
|
||||
.multi-line .item-value { line-height: 52rpx; padding-top: 4rpx; }
|
||||
.amount-item .item-value { color: #409eff; font-weight: 600; font-size: 32rpx; }
|
||||
.detail-content {
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 10px;
|
||||
padding: 15px;
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
.info-item { display: flex; justify-content: space-between; margin-top: 10px; }
|
||||
.info-label { color: #333; font-weight: bold; }
|
||||
.info-value { color: #666; }
|
||||
</style>
|
||||
|
||||
<style scoped>
|
||||
/* 主容器 - 与请假/报销/用印申请完全一致的卡片风格,精致阴影+大圆角+合理内边距 */
|
||||
.reimburse-detail-container {
|
||||
|
||||
@@ -134,10 +134,26 @@
|
||||
},
|
||||
methods: {
|
||||
formatTime(timeStr) {
|
||||
if (!timeStr) return '';
|
||||
const [datePart] = timeStr.split(' ');
|
||||
return datePart;
|
||||
if (!timeStr) return ''
|
||||
var str = String(timeStr).trim()
|
||||
// 兼容后端常见时间格式:"2026-01-08 00:00:00" / "2026-01-08T00:00:00" / 时间戳
|
||||
if (/^\d+$/.test(str)) {
|
||||
var ts = Number(str)
|
||||
if (!Number.isNaN(ts)) {
|
||||
var d = new Date(ts)
|
||||
if (!Number.isNaN(d.getTime())) {
|
||||
return this.pad(d.getFullYear(), 4) + '-' + this.pad(d.getMonth() + 1) + '-' + this.pad(d.getDate())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
str = str.replace('T', ' ')
|
||||
var parts = str.split(' ')
|
||||
return parts[0] || str
|
||||
},
|
||||
pad(n, len = 2) {
|
||||
return String(n).padStart(len, '0')
|
||||
},
|
||||
openPopup() {
|
||||
this.$refs.popup.open()
|
||||
},
|
||||
|
||||
@@ -43,9 +43,13 @@
|
||||
<view class="item-value">{{ detail.updateBy || '无' }}</view>
|
||||
</view>
|
||||
<view class="detail-item single-item">
|
||||
<text class="item-icon icon-date"></text>
|
||||
<view class="item-label">附件:</view>
|
||||
<view class="item-value">
|
||||
<text class="item-icon icon-date"></text>
|
||||
<view class="item-label">附件:</view>
|
||||
<view class="item-value">
|
||||
<view v-if="uploading" class="upload-loading">
|
||||
<text class="loading-dot"></text>
|
||||
<text class="loading-text">附件正在上传中...</text>
|
||||
</view>
|
||||
<oa-file-list :files="detail.accessoryApplyIds"></oa-file-list>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -19,9 +19,7 @@
|
||||
"modules" : {
|
||||
"VideoPlayer" : {},
|
||||
"Camera" : {},
|
||||
"Record" : {},
|
||||
"Geolocation" : {},
|
||||
"Maps" : {}
|
||||
"Record" : {}
|
||||
},
|
||||
"distribute" : {
|
||||
"android" : {
|
||||
@@ -68,7 +66,11 @@
|
||||
},
|
||||
"sdkConfigs" : {
|
||||
"ad" : {},
|
||||
"geolocation" : {},
|
||||
"geolocation" : {
|
||||
"system" : {
|
||||
"__platform__" : [ "ios", "android" ]
|
||||
}
|
||||
},
|
||||
"maps" : {},
|
||||
"share" : {},
|
||||
"statics" : {},
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
<view class="page-nav">
|
||||
<view class="nav-tabs nav-tabs--top">
|
||||
<view class="nav-tab" :class="{ active: activeTopTab === 'approval' }" @click="switchTopTab('approval')">我的审批</view>
|
||||
<view class="nav-tab" :class="{ active: activeTopTab === 'myApply' }" @click="switchTopTab('myApply')">我的申请</view>
|
||||
<view class="nav-tab" :class="{ active: activeTopTab === 'apply' }" @click="switchTopTab('apply')">发起申请</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -60,6 +61,52 @@
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<view v-else-if="activeTopTab === 'myApply'" class="tab-panel lower-panel my-apply-panel">
|
||||
<view class="sub-tabs sub-tabs--underline">
|
||||
<view class="sub-tab" :class="{ active: myApplyStatusTab === 'all' }" @click="switchMyApplyStatusTab('all')">全部</view>
|
||||
<view class="sub-tab" :class="{ active: myApplyStatusTab === 'running' }" @click="switchMyApplyStatusTab('running')">审批中</view>
|
||||
<view class="sub-tab" :class="{ active: myApplyStatusTab === 'approved' }" @click="switchMyApplyStatusTab('approved')">已通过</view>
|
||||
<view class="sub-tab" :class="{ active: myApplyStatusTab === 'rejected' }" @click="switchMyApplyStatusTab('rejected')">已驳回</view>
|
||||
</view>
|
||||
|
||||
<view class="type-row">
|
||||
<scroll-view class="type-scroll" scroll-x show-scrollbar="false">
|
||||
<view class="type-tabs">
|
||||
<view class="type-tab" :class="{ active: myApplyBizTypeIndex === 0 }" @click="setMyApplyBizType(0)">全部</view>
|
||||
<view class="type-tab" v-for="(item, index) in bizTypeList.slice(1)" :key="item.value" :class="{ active: myApplyBizTypeIndex === index + 1 }" @click="setMyApplyBizType(index + 1)">{{ item.label }}</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
<view class="refresh-row" @click="refreshCurrentList">
|
||||
<uni-icons type="refreshempty" size="18" color="#1677ff"></uni-icons>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<scroll-view class="approval-list" scroll-y>
|
||||
<view v-if="loading" class="loading-container">
|
||||
<uni-load-more type="loading" color="#409EFF"></uni-load-more>
|
||||
</view>
|
||||
<view v-else-if="myApplyList.length === 0" class="empty-container">
|
||||
<uni-icons type="empty" size="60" color="#909399"></uni-icons>
|
||||
<view class="empty-text">暂无我的申请</view>
|
||||
</view>
|
||||
<view v-else class="list-item" v-for="(item, index) in myApplyList" :key="item.instId || item.id || index" @click="goDetail(item)">
|
||||
<view class="item-tag" :class="getBizTypeTagType(item.bizType)">{{ getBizTypeText(item.bizType) }}</view>
|
||||
<view class="item-main">
|
||||
<view class="applicant">
|
||||
<uni-icons type="paperplane" size="14" color="#8a8f99"></uni-icons>
|
||||
{{ formatMyApplyTitle(item) }}
|
||||
</view>
|
||||
<view class="request-info">{{ formatMyApplyInfo(item) }}</view>
|
||||
<view class="time-info">{{ formatDate(item.createTime || item.applyTime || item.startTime) }}</view>
|
||||
</view>
|
||||
<view class="right-section">
|
||||
<view class="status-tag" :class="statusType(item.status)">{{ myApplyStatusText(item) }}</view>
|
||||
<view v-if="item.bizType === 'travel' && (item.status === 'approved' || item.status === 'finished') && !item.actualEndTime && !item.endTime && !item.realEndTime" class="early-end-btn" @click.stop="goTravelEarlyEnd(item)">提前结束</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<view v-else class="tab-panel apply-panel lower-panel">
|
||||
|
||||
<view class="apply-list">
|
||||
@@ -78,6 +125,7 @@
|
||||
<script>
|
||||
import { getUserProfile } from '@/api/oa/user.js';
|
||||
import { approveFlowTask, listMyFlowInstance, listTodoFlowTask, listDoneFlowTask, rejectFlowTask } from '@/api/hrm/flow';
|
||||
import { earlyEndTravelReq } from '@/api/hrm/travel';
|
||||
|
||||
export default {
|
||||
name: 'HrmApproval',
|
||||
@@ -89,14 +137,19 @@ export default {
|
||||
todoList: [],
|
||||
doneList: [],
|
||||
ccList: [],
|
||||
myApplyList: [],
|
||||
summary: {
|
||||
todo: 0,
|
||||
done: 0,
|
||||
cc: 0
|
||||
cc: 0,
|
||||
myApply: 0
|
||||
},
|
||||
query: {
|
||||
bizType: ''
|
||||
},
|
||||
myApplyQuery: {
|
||||
bizType: ''
|
||||
},
|
||||
bizTypeList: [
|
||||
{ label: '全部', value: '' },
|
||||
{ label: '请假', value: 'leave' },
|
||||
@@ -106,6 +159,8 @@ export default {
|
||||
{ label: '拨款', value: 'appropriation' }
|
||||
],
|
||||
bizTypeIndex: 0,
|
||||
myApplyBizTypeIndex: 0,
|
||||
myApplyStatusTab: 'all',
|
||||
currentOaUserId: '',
|
||||
roleGroup: [],
|
||||
applyTypes: [
|
||||
@@ -124,11 +179,13 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
currentList() {
|
||||
if (this.activeTopTab === 'myApply') return this.myApplyList;
|
||||
if (this.activeApprovalTab === 'todo') return this.todoList;
|
||||
if (this.activeApprovalTab === 'done') return this.doneList;
|
||||
return this.ccList;
|
||||
},
|
||||
emptyText() {
|
||||
if (this.activeTopTab === 'myApply') return '暂无我的申请';
|
||||
const map = {
|
||||
todo: '暂无待处理审批',
|
||||
done: '暂无已处理记录',
|
||||
@@ -149,6 +206,7 @@ export default {
|
||||
this.$store.commit('oa/SET_STATE', user);
|
||||
this.currentOaUserId = user.userId || '';
|
||||
this.roleGroup = roles;
|
||||
this.myApplyBizTypeIndex = 0;
|
||||
return user;
|
||||
} catch (error) {
|
||||
console.error('获取用户个人信息失败:', error);
|
||||
@@ -160,6 +218,10 @@ export default {
|
||||
this.activeTopTab = tab;
|
||||
if (tab === 'approval') {
|
||||
this.refreshCurrentList();
|
||||
return;
|
||||
}
|
||||
if (tab === 'myApply') {
|
||||
this.refreshCurrentList();
|
||||
}
|
||||
},
|
||||
switchApprovalTab(tab) {
|
||||
@@ -167,6 +229,10 @@ export default {
|
||||
this.refreshCurrentList();
|
||||
},
|
||||
refreshCurrentList() {
|
||||
if (this.activeTopTab === 'myApply') {
|
||||
this.loadMyApplyList();
|
||||
return;
|
||||
}
|
||||
if (this.activeTopTab !== 'approval') return;
|
||||
if (this.activeApprovalTab === 'todo') {
|
||||
this.loadTodoList();
|
||||
@@ -234,6 +300,35 @@ export default {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
async loadMyApplyList() {
|
||||
this.loading = true;
|
||||
try {
|
||||
const res = await listMyFlowInstance({ pageNum: 1, pageSize: 200 });
|
||||
let list = res.rows || res.data || [];
|
||||
if (this.myApplyQuery.bizType) list = list.filter(item => item.bizType === this.myApplyQuery.bizType);
|
||||
if (this.myApplyStatusTab !== 'all') {
|
||||
list = list.filter(item => this.matchMyApplyStatus(item.status));
|
||||
}
|
||||
this.myApplyList = list;
|
||||
this.summary.myApply = list.length;
|
||||
} catch (err) {
|
||||
console.error('加载我的申请失败:', err);
|
||||
this.myApplyList = [];
|
||||
this.summary.myApply = 0;
|
||||
uni.showToast({ title: '加载我的申请失败', icon: 'none' });
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
switchMyApplyStatusTab(tab) {
|
||||
this.myApplyStatusTab = tab;
|
||||
this.refreshCurrentList();
|
||||
},
|
||||
setMyApplyBizType(index) {
|
||||
this.myApplyQuery.bizType = this.bizTypeList[index].value;
|
||||
this.myApplyBizTypeIndex = index;
|
||||
this.refreshCurrentList();
|
||||
},
|
||||
getBizTypeText(type) {
|
||||
const map = { leave: '请假', travel: '出差', seal: '用印', reimburse: '报销', appropriation: '拨款' };
|
||||
return map[type] || type || '-';
|
||||
@@ -242,10 +337,36 @@ export default {
|
||||
const map = { leave: 'primary', travel: 'success', seal: 'warning', reimburse: 'danger', appropriation: 'danger' };
|
||||
return map[type] || 'info';
|
||||
},
|
||||
formatMyApplyTitle(item) {
|
||||
return item?.bizTitle || item?.title || this.getBizTypeText(item?.bizType) || '我的申请';
|
||||
},
|
||||
formatMyApplyInfo(item) {
|
||||
if (!item) return '-';
|
||||
if (item.bizType === 'leave') return item.subTitle || item.leaveType || '请假申请';
|
||||
if (item.bizType === 'travel') return item.subTitle || item.destination || '出差申请';
|
||||
if (item.bizType === 'seal') return item.subTitle || '用印申请';
|
||||
if (item.bizType === 'reimburse') return item.subTitle || '报销申请';
|
||||
if (item.bizType === 'appropriation') return item.subTitle || '拨款申请';
|
||||
return item.subTitle || item.bizTitle || '-';
|
||||
},
|
||||
matchMyApplyStatus(status) {
|
||||
const s = String(status || '').toLowerCase();
|
||||
if (this.myApplyStatusTab === 'running') return ['pending', 'running'].includes(s);
|
||||
if (this.myApplyStatusTab === 'approved') return ['approved', 'finished'].includes(s);
|
||||
if (this.myApplyStatusTab === 'rejected') return ['rejected', 'reject'].includes(s);
|
||||
return true;
|
||||
},
|
||||
statusText(status) {
|
||||
const map = { pending: '待审批', draft: '草稿', approved: '已通过', rejected: '已驳回', running: '审批中', finished: '已完成', revoked: '已撤销' };
|
||||
return map[status] || status || '-';
|
||||
},
|
||||
myApplyStatusText(item) {
|
||||
if (item.bizType === 'travel' && (item.status === 'approved' || item.status === 'finished')) {
|
||||
const endTime = item.actualEndTime || item.endTime || item.realEndTime;
|
||||
return endTime ? '已结束' : '出差中';
|
||||
}
|
||||
return this.statusText(item.status);
|
||||
},
|
||||
statusType(status) {
|
||||
const map = { pending: 'warning', running: 'warning', draft: 'info', approved: 'success', rejected: 'danger', finished: 'success', revoked: 'danger' };
|
||||
return map[status] || 'info';
|
||||
@@ -332,11 +453,40 @@ export default {
|
||||
this.refreshCurrentList();
|
||||
});
|
||||
break;
|
||||
|
||||
case 3:
|
||||
this.goTravelEarlyEnd(task);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
goTravelEarlyEnd(task) {
|
||||
if (!task || task.bizType !== 'travel') {
|
||||
uni.showToast({ title: '仅出差申请支持提前结束', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
const bizId = task.bizId || task.instId;
|
||||
if (!bizId) {
|
||||
uni.showToast({ title: '缺少出差单编号', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
uni.showModal({
|
||||
title: '提前结束出差',
|
||||
content: '确定要提前结束该出差吗?',
|
||||
success: (res) => {
|
||||
if (!res.confirm) return;
|
||||
earlyEndTravelReq(bizId)
|
||||
.then(() => {
|
||||
uni.showToast({ title: '提前结束成功', icon: 'none' });
|
||||
this.refreshCurrentList();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('提前结束失败:', err);
|
||||
uni.showToast({ title: '提前结束失败', icon: 'none' });
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -582,6 +732,25 @@ export default {
|
||||
.request-info { font-size: 26rpx; color: #6b7280; }
|
||||
.time-info { font-size: 24rpx; color: #9ca3af; }
|
||||
|
||||
.early-end-btn {
|
||||
flex: 0 0 auto;
|
||||
padding: 6rpx 16rpx;
|
||||
font-size: 22rpx;
|
||||
color: #1677ff;
|
||||
background: #eef4ff;
|
||||
border-radius: 6rpx;
|
||||
border: 1rpx solid #1677ff;
|
||||
}
|
||||
|
||||
.right-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
gap: 8rpx;
|
||||
padding-right: 8rpx;
|
||||
}
|
||||
|
||||
.apply-panel {
|
||||
padding-top: 16rpx;
|
||||
}
|
||||
|
||||
@@ -1,43 +1,145 @@
|
||||
<template>
|
||||
<request-form
|
||||
title="拨款申请"
|
||||
subtitle="填写拨款信息,支持手机端快速提交"
|
||||
biz-type="appropriation"
|
||||
:request-api="submitApply"
|
||||
:initial-form="initialForm"
|
||||
:sections="sections"
|
||||
:flow-fields="flowFields"
|
||||
/>
|
||||
<view class="page">
|
||||
<view class="card">
|
||||
<view class="card-title">基础信息</view>
|
||||
|
||||
<view class="field">
|
||||
<text class="label">拨款类型<text class="req">*</text></text>
|
||||
<picker mode="selector" :range="appropriationTypes" @change="onAppropriationTypeChange">
|
||||
<view class="input-like clickable">{{ appropriationType || '请选择拨款类型' }}</view>
|
||||
</picker>
|
||||
</view>
|
||||
|
||||
<view class="field">
|
||||
<text class="label">拨款金额<text class="req">*</text></text>
|
||||
<input class="input" type="text" :value="amount" placeholder="请输入金额" @input="onAmountInput" />
|
||||
</view>
|
||||
|
||||
<view class="field">
|
||||
<text class="label">附件</text>
|
||||
<view class="input-like clickable" @tap="openFilePicker">点击上传附件</view>
|
||||
<view v-if="uploadedFiles.length" class="file-list">
|
||||
<view v-for="(file, idx) in uploadedFiles" :key="file.ossId || file.url || idx" class="file-item">
|
||||
<text class="file-name">{{ file.originalName || file.fileName || file.name || file.url }}</text>
|
||||
<text class="remove" @tap.stop="removeFile(idx)">删除</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="card">
|
||||
<view class="card-title">说明</view>
|
||||
<view class="field">
|
||||
<text class="label">用途说明<text class="req">*</text></text>
|
||||
<textarea v-model="reason" class="textarea" placeholder="请说明拨款用途与依据" />
|
||||
</view>
|
||||
<view class="field">
|
||||
<text class="label">备注</text>
|
||||
<textarea v-model="remark" class="textarea" placeholder="可选" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="card">
|
||||
<button class="btn primary" :loading="submitting" @tap="submit">提交申请</button>
|
||||
</view>
|
||||
|
||||
<x-file-picker ref="globalFilePicker" @select="onGlobalFileSelect" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import RequestForm from '@/components/hrm/RequestForm.vue'
|
||||
import { addAppropriationReq } from '@/api/hrm/appropriation'
|
||||
import { uploadFile } from '@/api/common/upload'
|
||||
import XFilePicker from '@/components/x-native-uploader/x-file-picker.vue'
|
||||
|
||||
export default {
|
||||
components: { RequestForm },
|
||||
components: { XFilePicker },
|
||||
data() {
|
||||
return {
|
||||
initialForm: { appropriationType: '', amount: '', reason: '', remark: '' },
|
||||
sections: [
|
||||
{ key: 'basic', title: '基础信息', fields: [
|
||||
{ key: 'appropriationType', label: '拨款类型', type: 'select', required: true, placeholder: '请选择拨款类型', options: ['项目拨款', '部门拨款', '专项拨款', '备用金拨款', '其他'] },
|
||||
{ key: 'amount', label: '拨款金额', type: 'input', inputType: 'digit', required: true, placeholder: '请输入金额' }
|
||||
]},
|
||||
{ key: 'desc', title: '说明', fields: [
|
||||
{ key: 'reason', label: '用途说明', type: 'textarea', required: true, placeholder: '请说明拨款用途与依据' },
|
||||
{ key: 'remark', label: '备注', type: 'textarea', required: false, placeholder: '可选' }
|
||||
]}
|
||||
],
|
||||
flowFields: [
|
||||
{ key: 'appropriationType', label: '拨款类型', required: true },
|
||||
{ key: 'amount', label: '拨款金额', required: true },
|
||||
{ key: 'reason', label: '用途说明', required: true }
|
||||
]
|
||||
appropriationTypes: ['项目拨款', '部门拨款', '专项拨款', '备用金拨款', '其他'],
|
||||
appropriationType: '',
|
||||
amount: '',
|
||||
reason: '',
|
||||
remark: '',
|
||||
accessoryApplyIds: '',
|
||||
uploadedFiles: [],
|
||||
submitting: false
|
||||
}
|
||||
},
|
||||
methods: { submitApply(payload) { return addAppropriationReq(payload) } }
|
||||
methods: {
|
||||
onAppropriationTypeChange(e) {
|
||||
var idx = Number(e.detail.value)
|
||||
this.appropriationType = this.appropriationTypes[idx] || ''
|
||||
},
|
||||
onAmountInput(e) {
|
||||
this.amount = e.detail.value
|
||||
},
|
||||
openFilePicker() {
|
||||
var picker = this.$refs.globalFilePicker
|
||||
if (picker && picker.open) picker.open()
|
||||
},
|
||||
async onGlobalFileSelect(payload) {
|
||||
var files = Array.isArray(payload) ? payload : [payload]
|
||||
for (var i = 0; i < files.length; i++) {
|
||||
var f = files[i]
|
||||
if (!f) continue
|
||||
try {
|
||||
var uploaded = await uploadFile({
|
||||
path: f.path || f.url || '',
|
||||
name: f.name || f.fileName || 'file',
|
||||
size: f.size || 0,
|
||||
type: f.type || ''
|
||||
})
|
||||
this.uploadedFiles.push({
|
||||
name: uploaded.fileName || f.name || f.fileName || 'file',
|
||||
fileName: uploaded.fileName || f.fileName || f.name || '',
|
||||
originalName: f.name || f.fileName || uploaded.fileName || '',
|
||||
url: uploaded.url || f.path || f.url || '',
|
||||
ossId: uploaded.ossId || uploaded.url || f.path || f.url || ''
|
||||
})
|
||||
} catch (e) {
|
||||
uni.showToast({ title: e.message || '上传失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
this.syncFileIds()
|
||||
},
|
||||
removeFile(idx) {
|
||||
this.uploadedFiles.splice(idx, 1)
|
||||
this.syncFileIds()
|
||||
},
|
||||
syncFileIds() {
|
||||
this.accessoryApplyIds = this.uploadedFiles.map(function (f) { return f.ossId }).join(',')
|
||||
},
|
||||
async submit() {
|
||||
if (!this.appropriationType) return uni.showToast({ title: '请选择拨款类型', icon: 'none' })
|
||||
if (!this.amount) return uni.showToast({ title: '请输入拨款金额', icon: 'none' })
|
||||
if (!this.reason) return uni.showToast({ title: '请填写用途说明', icon: 'none' })
|
||||
this.submitting = true
|
||||
try {
|
||||
await addAppropriationReq({
|
||||
appropriationType: this.appropriationType,
|
||||
amount: this.amount,
|
||||
accessoryApplyIds: this.accessoryApplyIds,
|
||||
reason: this.reason,
|
||||
remark: this.remark,
|
||||
status: 'pending',
|
||||
bizType: 'appropriation'
|
||||
})
|
||||
uni.showToast({ title: '提交成功', icon: 'success' })
|
||||
setTimeout(() => uni.navigateBack({ delta: 1 }), 500)
|
||||
} catch (e) {
|
||||
uni.showToast({ title: e.message || '提交失败', icon: 'none' })
|
||||
} finally {
|
||||
this.submitting = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
<style scoped>
|
||||
.page{min-height:100vh;background:#f5f7fb;padding:24rpx;box-sizing:border-box}
|
||||
.card{background:#fff;border-radius:20rpx;padding:24rpx;margin-bottom:20rpx}
|
||||
.card-title{font-size:30rpx;font-weight:700;color:#111827;margin-bottom:16rpx}
|
||||
.field{margin-bottom:18rpx}.label{display:block;margin-bottom:8rpx;font-size:24rpx;color:#6b7280}.req{color:#ef4444;margin-left:4rpx}.input-like,.input,.textarea{width:100%;box-sizing:border-box;border:1rpx solid #e5e7eb;border-radius:16rpx;background:#f9fafb;padding:20rpx;font-size:28rpx;color:#111827}.input{min-height:88rpx}.clickable{color:#111827}.textarea{min-height:160rpx}.file-list{margin-top:16rpx;display:flex;flex-direction:column;gap:12rpx}.file-item{display:flex;align-items:center;justify-content:space-between;padding:18rpx 18rpx;border-radius:16rpx;background:#f8fafc;border:1rpx solid #e5eefc}.file-name{flex:1;font-size:26rpx;color:#334155;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;margin-right:16rpx}.remove{font-size:24rpx;color:#ef4444;flex-shrink:0}.btn.primary{width:100%;background:#1677ff;color:#fff;border-radius:16rpx}
|
||||
</style>
|
||||
@@ -2,10 +2,6 @@
|
||||
<view class="hrm-page">
|
||||
<!-- 顶部统计栏 -->
|
||||
<view class="summary-bar">
|
||||
<view class="summary-left">
|
||||
<view class="page-title">审批中心</view>
|
||||
<view class="page-desc">集中查看与处理待办审批</view>
|
||||
</view>
|
||||
<view class="summary-right">
|
||||
<view class="metric">
|
||||
<view class="metric-value">{{ todoCount }}</view>
|
||||
|
||||
@@ -1,13 +1,58 @@
|
||||
<template>
|
||||
<view class="approval-detail-page">
|
||||
<!-- 动态渲染不同类型的审批详情组件 -->
|
||||
<view class="flow-summary-card" v-if="detailData">
|
||||
<view class="summary-head">
|
||||
<text class="summary-title">审批信息</text>
|
||||
<text class="status-tag" :class="statusClass(detailData.flowStatus)">{{ statusText(detailData.flowStatus) }}</text>
|
||||
</view>
|
||||
<view class="summary-grid">
|
||||
<view class="summary-item">
|
||||
<text class="label">是否通过</text>
|
||||
<text class="value">{{ detailData.approved ? '已通过' : (detailData.flowStatus === 'rejected' ? '已驳回' : '待审批') }}</text>
|
||||
</view>
|
||||
<view class="summary-item">
|
||||
<text class="label">当前状态</text>
|
||||
<text class="value">{{ statusText(detailData.flowStatus) }}</text>
|
||||
</view>
|
||||
<view class="summary-item">
|
||||
<text class="label">当前节点</text>
|
||||
<text class="value">{{ detailData.currentNodeName || detailData.currentNodeId || '-' }}</text>
|
||||
</view>
|
||||
<view class="summary-item">
|
||||
<text class="label">当前审批人</text>
|
||||
<text class="value">{{ assigneeText }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="flow-history-card" v-if="detailData && detailData.actionTimeline && detailData.actionTimeline.length">
|
||||
<view class="card-title">审批流程</view>
|
||||
<view class="timeline-item" v-for="(item, index) in detailData.actionTimeline" :key="item.actionId || index">
|
||||
<view class="timeline-dot" :class="item.action"></view>
|
||||
<view class="timeline-line" v-if="index !== detailData.actionTimeline.length - 1"></view>
|
||||
<view class="timeline-content">
|
||||
<view class="timeline-header">
|
||||
<text class="timeline-name">{{ item.actionUserName || '未知审批人' }}</text>
|
||||
<text class="timeline-time">{{ formatTime(item.createTime) }}</text>
|
||||
</view>
|
||||
<view class="timeline-meta">
|
||||
<text>节点:{{ item.nodeName || item.nodeId || '-' }}</text>
|
||||
<text>动作:{{ item.actionText || item.action || '-' }}</text>
|
||||
</view>
|
||||
<view class="timeline-meta">
|
||||
<text>任务状态:{{ statusText(item.taskStatus) }}</text>
|
||||
</view>
|
||||
<view class="timeline-remark" v-if="item.remark">意见:{{ item.remark }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<component
|
||||
:is="currentDetailComponent"
|
||||
:bizId="bizId"
|
||||
v-if="bizId && bizType"
|
||||
></component>
|
||||
|
||||
<!-- 底部固定的审批操作按钮栏 -->
|
||||
<view class="approval-btn-bar" v-if="canApprove">
|
||||
<button class="btn reject-btn" @click="handleReject">驳回</button>
|
||||
<button class="btn approve-btn" @click="handleApprove">通过</button>
|
||||
@@ -21,17 +66,14 @@
|
||||
import HRMSealDetail from '@/components/hrm/detailPanels/seal.vue'
|
||||
import HRMTravelDetail from '@/components/hrm/detailPanels/travel.vue'
|
||||
import HRMAppropriationDetail from '@/components/hrm/detailPanels/appropriation.vue'
|
||||
|
||||
import {
|
||||
approveFlowTask,
|
||||
listTodoFlowTask,
|
||||
rejectFlowTask,
|
||||
getTodoTaskByBiz,
|
||||
getFlowTaskDetailByBiz,
|
||||
} from '@/api/hrm/flow';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
// 注册所有详情组件,供动态组件使用
|
||||
HRMLeaveDetail,
|
||||
HRMReimburseDetail,
|
||||
HRMSealDetail,
|
||||
@@ -42,73 +84,82 @@
|
||||
return {
|
||||
bizId: undefined,
|
||||
bizType: undefined,
|
||||
currentTask: undefined,
|
||||
// 映射bizType到对应的组件名(需和你的bizType实际值匹配,可自行调整)
|
||||
detailData: null,
|
||||
bizTypeComponentMap: {
|
||||
leave: 'HRMLeaveDetail', // 请假
|
||||
reimburse: 'HRMReimburseDetail', // 报销
|
||||
seal: 'HRMSealDetail', // 用章
|
||||
travel: 'HRMTravelDetail' ,// 差旅
|
||||
leave: 'HRMLeaveDetail',
|
||||
reimburse: 'HRMReimburseDetail',
|
||||
seal: 'HRMSealDetail',
|
||||
travel: 'HRMTravelDetail',
|
||||
appropriation: 'HRMAppropriationDetail'
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
// 计算属性:根据bizType获取当前要渲染的组件名
|
||||
currentDetailComponent() {
|
||||
return this.bizTypeComponentMap[this.bizType] || '';
|
||||
},
|
||||
canApprove() {
|
||||
console.log(this.currentTask, this.$store.getters.storeOaName, this.$store.getters.storeOaId)
|
||||
return this.currentTask && this.currentTask.status === 'pending' &&
|
||||
(this.currentTask?.assigneeUserName === this.$store.getters.storeOaName
|
||||
|| this.currentTask?.assigneeUserId === this.$store.getters.storeOaId)
|
||||
currentTask() {
|
||||
return this.detailData?.currentTask || null;
|
||||
},
|
||||
canApprove() {
|
||||
return this.currentTask && this.currentTask.status === 'pending' &&
|
||||
(this.currentTask?.assigneeUserName === this.$store.getters.storeOaName
|
||||
|| this.currentTask?.assigneeUserId === this.$store.getters.storeOaId)
|
||||
},
|
||||
assigneeText() {
|
||||
return this.currentTask?.assigneeNickName || this.currentTask?.assigneeUserName || this.currentTask?.assigneeUserId || '-';
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
bizId: {
|
||||
immediate: true, // 页面加载时立即执行(原代码缺失,导致首次赋值不触发)
|
||||
immediate: true,
|
||||
handler(newVal) {
|
||||
if (!newVal || !this.bizType) return;
|
||||
// 获取当前审批任务信息
|
||||
getTodoTaskByBiz(this.bizType, newVal)
|
||||
getFlowTaskDetailByBiz(this.bizType, newVal)
|
||||
.then(res => {
|
||||
this.currentTask = res.data;
|
||||
this.detailData = res.data || null;
|
||||
})
|
||||
.catch(err => {
|
||||
uni.showToast({
|
||||
title: '获取审批信息失败',
|
||||
icon: 'none'
|
||||
});
|
||||
console.error('获取审批任务失败:', err);
|
||||
console.error('获取审批详情失败:', err);
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* 审批通过
|
||||
*/
|
||||
statusText(status) {
|
||||
const map = {
|
||||
pending: '审批中',
|
||||
running: '流转中',
|
||||
approved: '已通过',
|
||||
rejected: '已驳回',
|
||||
reject: '已驳回',
|
||||
withdraw: '已撤回',
|
||||
withdrawn: '已撤回'
|
||||
};
|
||||
return map[status] || status || '-';
|
||||
},
|
||||
statusClass(status) {
|
||||
return status || 'default';
|
||||
},
|
||||
formatTime(time) {
|
||||
if (!time) return '-';
|
||||
const d = new Date(time);
|
||||
return Number.isNaN(d.getTime()) ? time : `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')} ${String(d.getHours()).padStart(2,'0')}:${String(d.getMinutes()).padStart(2,'0')}`;
|
||||
},
|
||||
handleApprove() {
|
||||
if (!this.currentTask?.taskId) {
|
||||
uni.showToast({ title: '暂无审批任务', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
|
||||
// 二次确认
|
||||
uni.showModal({
|
||||
title: '确认通过',
|
||||
content: '是否确定通过该审批?',
|
||||
// 箭头函数保留this指向
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
approveFlowTask(this.currentTask.taskId)
|
||||
.then(() => {
|
||||
uni.showToast({ title: '审批通过成功' });
|
||||
// 成功后返回上一页(可根据需求调整)
|
||||
setTimeout(() => {
|
||||
uni.navigateBack();
|
||||
}, 1500);
|
||||
setTimeout(() => uni.navigateBack(), 1500);
|
||||
})
|
||||
.catch(err => {
|
||||
uni.showToast({ title: '审批通过失败', icon: 'none' });
|
||||
@@ -118,17 +169,11 @@
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 审批驳回
|
||||
*/
|
||||
handleReject() {
|
||||
if (!this.currentTask?.taskId) {
|
||||
uni.showToast({ title: '暂无审批任务', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
|
||||
// 二次确认(可扩展:添加驳回理由输入框)
|
||||
uni.showModal({
|
||||
title: '确认驳回',
|
||||
content: '是否确定驳回该审批?',
|
||||
@@ -137,10 +182,7 @@
|
||||
rejectFlowTask(this.currentTask.taskId)
|
||||
.then(() => {
|
||||
uni.showToast({ title: '审批驳回成功' });
|
||||
// 成功后返回上一页(可根据需求调整)
|
||||
setTimeout(() => {
|
||||
uni.navigateBack();
|
||||
}, 1500);
|
||||
setTimeout(() => uni.navigateBack(), 1500);
|
||||
})
|
||||
.catch(err => {
|
||||
uni.showToast({ title: '审批驳回失败', icon: 'none' });
|
||||
@@ -151,14 +193,9 @@
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
onLoad(options) {
|
||||
console.log('页面入参:', options);
|
||||
// 校验入参,避免undefined
|
||||
this.bizId = options.bizId || '';
|
||||
this.bizType = options.bizType || '';
|
||||
|
||||
// 入参缺失提示
|
||||
if (!this.bizId || !this.bizType) {
|
||||
uni.showToast({ title: '参数缺失,无法加载详情', icon: 'none' });
|
||||
}
|
||||
@@ -167,46 +204,60 @@
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 页面容器 */
|
||||
.approval-detail-page {
|
||||
min-height: 100vh;
|
||||
padding-bottom: 120rpx; /* 给底部按钮栏留空间 */
|
||||
padding: 24rpx 24rpx 140rpx;
|
||||
background: #f7f8fa;
|
||||
}
|
||||
|
||||
/* 底部审批按钮栏 */
|
||||
.approval-btn-bar {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
.flow-summary-card, .flow-history-card {
|
||||
background: #fff;
|
||||
border-radius: 24rpx;
|
||||
padding: 24rpx;
|
||||
margin-bottom: 24rpx;
|
||||
box-shadow: 0 8rpx 30rpx rgba(0,0,0,0.04);
|
||||
}
|
||||
.summary-head {
|
||||
display: flex;
|
||||
padding: 20rpx;
|
||||
background-color: #fff;
|
||||
border-top: 1px solid #eee;
|
||||
z-index: 99;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
/* 按钮通用样式 */
|
||||
.btn {
|
||||
flex: 1;
|
||||
height: 88rpx;
|
||||
line-height: 88rpx;
|
||||
border-radius: 44rpx;
|
||||
.summary-title, .card-title {
|
||||
font-size: 32rpx;
|
||||
border: none;
|
||||
margin: 0 10rpx;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
/* 驳回按钮样式 */
|
||||
.reject-btn {
|
||||
background-color: #fff;
|
||||
color: #ff4757;
|
||||
border: 1px solid #ff4757;
|
||||
.status-tag {
|
||||
padding: 8rpx 16rpx;
|
||||
border-radius: 999rpx;
|
||||
font-size: 24rpx;
|
||||
background: #eef2ff;
|
||||
color: #4f46e5;
|
||||
}
|
||||
|
||||
/* 通过按钮样式 */
|
||||
.approve-btn {
|
||||
background-color: #007aff;
|
||||
color: #fff;
|
||||
.summary-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16rpx;
|
||||
}
|
||||
</style>
|
||||
.summary-item {
|
||||
width: calc(50% - 8rpx);
|
||||
background: #f8fafc;
|
||||
border-radius: 16rpx;
|
||||
padding: 20rpx;
|
||||
}
|
||||
.label { display:block; color:#6b7280; font-size:24rpx; margin-bottom: 8rpx; }
|
||||
.value { display:block; color:#111827; font-size:28rpx; font-weight:500; }
|
||||
.timeline-item { position: relative; display:flex; padding-top: 24rpx; }
|
||||
.timeline-dot { width: 18rpx; height:18rpx; border-radius:50%; background:#cbd5e1; margin-right: 16rpx; margin-top: 8rpx; flex-shrink:0; }
|
||||
.timeline-dot.approve, .timeline-dot.approved, .timeline-dot.running { background:#22c55e; }
|
||||
.timeline-dot.reject, .timeline-dot.rejected { background:#ef4444; }
|
||||
.timeline-line { position:absolute; left: 8rpx; top: 36rpx; bottom:-4rpx; width:2rpx; background:#e5e7eb; }
|
||||
.timeline-content { flex:1; padding-bottom: 8rpx; }
|
||||
.timeline-header, .timeline-meta { display:flex; justify-content:space-between; gap: 16rpx; font-size:24rpx; color:#6b7280; }
|
||||
.timeline-name { color:#111827; font-weight:600; }
|
||||
.timeline-remark { margin-top: 8rpx; color:#374151; font-size:26rpx; }
|
||||
.approval-btn-bar { position: fixed; bottom: 0; left: 0; right: 0; display: flex; padding: 20rpx; background-color: #fff; border-top: 1px solid #eee; z-index: 99; }
|
||||
.btn { flex: 1; height: 88rpx; line-height: 88rpx; border-radius: 44rpx; font-size: 32rpx; border: none; margin: 0 10rpx; }
|
||||
.reject-btn { background-color: #fff; color: #ff4757; border: 1px solid #ff4757; }
|
||||
.approve-btn { background-color: #007aff; color: #fff; }
|
||||
</style>
|
||||
|
||||
@@ -42,7 +42,7 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
submitLeave(payload) { return addLeaveReq(payload) }
|
||||
submitLeave(payload) { return addLeaveReq({ ...payload, status: 'pending', bizType: 'leave' }) }
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,44 +1,142 @@
|
||||
<template>
|
||||
<request-form
|
||||
title="报销申请"
|
||||
subtitle="填写报销信息,支持手机端快速提交"
|
||||
biz-type="reimburse"
|
||||
:request-api="submitReimburse"
|
||||
:initial-form="initialForm"
|
||||
:sections="sections"
|
||||
:flow-fields="flowFields"
|
||||
/>
|
||||
<view class="page">
|
||||
<view class="card">
|
||||
<view class="card-title">基础信息</view>
|
||||
|
||||
<view class="field">
|
||||
<text class="label">报销类型<text class="req">*</text></text>
|
||||
<picker mode="selector" :range="reimburseTypes" @change="onReimburseTypeChange">
|
||||
<view class="input-like clickable">{{ reimburseType || '请选择报销类型' }}</view>
|
||||
</picker>
|
||||
</view>
|
||||
|
||||
<view class="field">
|
||||
<text class="label">报销金额<text class="req">*</text></text>
|
||||
<input class="input" type="text" :value="totalAmount" placeholder="请输入金额" @input="onAmountInput" />
|
||||
</view>
|
||||
|
||||
<view class="field">
|
||||
<text class="label">附件</text>
|
||||
<view class="input-like clickable" @tap="openFilePicker">点击上传附件</view>
|
||||
<view v-if="uploadedFiles.length" class="file-list">
|
||||
<view v-for="(file, idx) in uploadedFiles" :key="file.ossId || file.url || idx" class="file-item">
|
||||
<text class="file-name">{{ file.originalName || file.fileName || file.name || file.url }}</text>
|
||||
<text class="remove" @tap.stop="removeFile(idx)">删除</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="card">
|
||||
<view class="card-title">说明</view>
|
||||
<view class="field">
|
||||
<text class="label">事由<text class="req">*</text></text>
|
||||
<textarea v-model="reason" class="textarea" placeholder="请说明报销用途" />
|
||||
</view>
|
||||
<view class="field">
|
||||
<text class="label">备注</text>
|
||||
<textarea v-model="remark" class="textarea" placeholder="可选" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="card">
|
||||
<button class="btn primary" :loading="submitting" @tap="submit">提交申请</button>
|
||||
</view>
|
||||
|
||||
<x-file-picker ref="globalFilePicker" @select="onGlobalFileSelect" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import RequestForm from '@/components/hrm/RequestForm.vue'
|
||||
import { addReimburseReq } from '@/api/hrm/reimburse'
|
||||
import { uploadFile } from '@/api/common/upload'
|
||||
import XFilePicker from '@/components/x-native-uploader/x-file-picker.vue'
|
||||
|
||||
export default {
|
||||
components: { RequestForm },
|
||||
components: { XFilePicker },
|
||||
data() {
|
||||
return {
|
||||
initialForm: { reimburseType: '', totalAmount: '', applyFileIds: '', reason: '', remark: '' },
|
||||
sections: [
|
||||
{ key: 'basic', title: '基础信息', fields: [
|
||||
{ key: 'reimburseType', label: '报销类型', type: 'select', required: true, placeholder: '请选择报销类型', options: ['差旅报销', '招待报销', '采购报销', '办公报销', '其他'] },
|
||||
{ key: 'totalAmount', label: '报销金额', type: 'input', inputType: 'digit', required: true, placeholder: '请输入金额' },
|
||||
{ key: 'applyFileIds', label: '附件', type: 'file', required: false, placeholder: '上传附件文件' }
|
||||
]},
|
||||
{ key: 'desc', title: '说明', fields: [
|
||||
{ key: 'reason', label: '事由', type: 'textarea', required: true, placeholder: '请说明报销用途' },
|
||||
{ key: 'remark', label: '备注', type: 'textarea', required: false, placeholder: '可选' }
|
||||
]}
|
||||
],
|
||||
flowFields: [
|
||||
{ key: 'reimburseType', label: '报销类型', required: true },
|
||||
{ key: 'totalAmount', label: '报销金额', required: true },
|
||||
{ key: 'reason', label: '事由', required: true }
|
||||
]
|
||||
reimburseTypes: ['差旅报销', '招待报销', '采购报销', '办公报销', '其他'],
|
||||
reimburseType: '',
|
||||
totalAmount: '',
|
||||
reason: '',
|
||||
remark: '',
|
||||
applyFileIds: '',
|
||||
uploadedFiles: [],
|
||||
submitting: false
|
||||
}
|
||||
},
|
||||
methods: { submitReimburse(payload) { return addReimburseReq(payload) } }
|
||||
methods: {
|
||||
onReimburseTypeChange(e) {
|
||||
var idx = Number(e.detail.value)
|
||||
this.reimburseType = this.reimburseTypes[idx] || ''
|
||||
},
|
||||
onAmountInput(e) {
|
||||
this.totalAmount = e.detail.value
|
||||
},
|
||||
openFilePicker() {
|
||||
var picker = this.$refs.globalFilePicker
|
||||
if (picker && picker.open) picker.open()
|
||||
},
|
||||
async onGlobalFileSelect(payload) {
|
||||
var files = Array.isArray(payload) ? payload : [payload]
|
||||
for (var i = 0; i < files.length; i++) {
|
||||
var f = files[i]
|
||||
if (!f) continue
|
||||
try {
|
||||
var uploaded = await uploadFile({
|
||||
path: f.path || f.url || '',
|
||||
name: f.name || f.fileName || 'file',
|
||||
size: f.size || 0,
|
||||
type: f.type || ''
|
||||
})
|
||||
this.uploadedFiles.push({
|
||||
name: uploaded.fileName || f.name || f.fileName || 'file',
|
||||
fileName: uploaded.fileName || f.fileName || f.name || '',
|
||||
originalName: f.name || f.fileName || uploaded.fileName || '',
|
||||
url: uploaded.url || f.path || f.url || '',
|
||||
ossId: uploaded.ossId || uploaded.url || f.path || f.url || ''
|
||||
})
|
||||
} catch (e) {
|
||||
uni.showToast({ title: e.message || '上传失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
this.syncFileIds()
|
||||
},
|
||||
removeFile(idx) {
|
||||
this.uploadedFiles.splice(idx, 1)
|
||||
this.syncFileIds()
|
||||
},
|
||||
syncFileIds() {
|
||||
this.applyFileIds = this.uploadedFiles.map(function (f) { return f.ossId }).join(',')
|
||||
},
|
||||
async submit() {
|
||||
if (!this.reimburseType) return uni.showToast({ title: '请选择报销类型', icon: 'none' })
|
||||
if (!this.totalAmount) return uni.showToast({ title: '请输入报销金额', icon: 'none' })
|
||||
if (!this.reason) return uni.showToast({ title: '请填写事由', icon: 'none' })
|
||||
this.submitting = true
|
||||
try {
|
||||
await addReimburseReq({
|
||||
reimburseType: this.reimburseType,
|
||||
totalAmount: this.totalAmount,
|
||||
applyFileIds: this.applyFileIds,
|
||||
reason: this.reason,
|
||||
remark: this.remark,
|
||||
status: 'pending',
|
||||
bizType: 'reimburse'
|
||||
})
|
||||
uni.showToast({ title: '提交成功', icon: 'success' })
|
||||
setTimeout(() => uni.navigateBack({ delta: 1 }), 500)
|
||||
} catch (e) {
|
||||
uni.showToast({ title: e.message || '提交失败', icon: 'none' })
|
||||
} finally {
|
||||
this.submitting = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
<style scoped>
|
||||
.page{min-height:100vh;background:#f5f7fb;padding:24rpx;box-sizing:border-box}.card{background:#fff;border-radius:20rpx;padding:24rpx;margin-bottom:20rpx}.field{margin-bottom:18rpx}.label{display:block;margin-bottom:8rpx;font-size:24rpx;color:#6b7280}.req{color:#ef4444;margin-left:4rpx}.input-like,.input,.textarea{width:100%;box-sizing:border-box;border:1rpx solid #e5e7eb;border-radius:16rpx;background:#f9fafb;padding:20rpx;font-size:28rpx;color:#111827}.input{min-height:88rpx}.clickable{color:#111827}.textarea{min-height:160rpx}.file-list{margin-top:16rpx;display:flex;flex-direction:column;gap:12rpx}.file-item{display:flex;align-items:center;justify-content:space-between;padding:18rpx 18rpx;border-radius:16rpx;background:#f8fafc;border:1rpx solid #e5eefc}.file-name{flex:1;font-size:26rpx;color:#334155;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;margin-right:16rpx}.remove{font-size:24rpx;color:#ef4444;flex-shrink:0}.btn.primary{width:100%;background:#1677ff;color:#fff;border-radius:16rpx}
|
||||
</style>
|
||||
@@ -22,7 +22,7 @@ export default {
|
||||
sections: [
|
||||
{ key: 'basic', title: '基础信息', fields: [
|
||||
{ key: 'sealType', label: '用印类型', type: 'select', required: true, placeholder: '请选择用印类型', options: ['合同用印', '公章', '财务章', '法人章', '其他'] },
|
||||
{ key: 'applyFileIds', label: '附件', type: 'file', required: true, placeholder: '上传盖章文件' }
|
||||
{ key: 'applyFileIds', label: '附件', type: 'file', required: true, placeholder: '上传盖章文件', accept: '.pdf' }
|
||||
]},
|
||||
{ key: 'desc', title: '说明', fields: [
|
||||
{ key: 'reason', label: '用途说明', type: 'textarea', required: true, placeholder: '请说明盖章用途与背景' },
|
||||
@@ -36,7 +36,7 @@ export default {
|
||||
]
|
||||
}
|
||||
},
|
||||
methods: { submitSeal(payload) { return addSealReq(payload) } }
|
||||
methods: { submitSeal(payload) { return addSealReq({ ...payload, status: 'pending', bizType: 'seal' }) } }
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,48 +1,301 @@
|
||||
<template>
|
||||
<request-form
|
||||
title="出差申请"
|
||||
subtitle="填写出差信息,支持手机端快速提交"
|
||||
biz-type="travel"
|
||||
:request-api="submitTravel"
|
||||
:initial-form="initialForm"
|
||||
:sections="sections"
|
||||
:flow-fields="flowFields"
|
||||
/>
|
||||
<view class="page">
|
||||
<view class="card">
|
||||
<view class="card-title">基础信息</view>
|
||||
|
||||
<view class="field">
|
||||
<text class="label">出差类型<text class="req">*</text></text>
|
||||
<picker mode="selector" :range="travelTypes" @change="onTravelTypeChange">
|
||||
<view class="input-like clickable">{{ travelType || '请选择出差类型' }}</view>
|
||||
</picker>
|
||||
</view>
|
||||
|
||||
<view class="field">
|
||||
<text class="label">开始时间<text class="req">*</text></text>
|
||||
<view class="input-like clickable" @tap="openDateTime('startTime')">{{ formatDateTime(startTime) || '请选择开始时间' }}</view>
|
||||
</view>
|
||||
|
||||
<view class="field">
|
||||
<text class="label">结束时间<text class="req">*</text></text>
|
||||
<view class="input-like clickable" @tap="openDateTime('endTime')">{{ formatDateTime(endTime) || '请选择结束时间' }}</view>
|
||||
</view>
|
||||
|
||||
<view class="field">
|
||||
<text class="label">目的地<text class="req">*</text></text>
|
||||
<view class="input-like clickable" @tap="openCityPicker">{{ destination || '请选择城市' }}</view>
|
||||
</view>
|
||||
|
||||
<view class="field">
|
||||
<text class="label">预估费用</text>
|
||||
<input class="input" type="text" :value="estimatedCost" placeholder="请输入预估费用" @input="onEstimatedCostInput" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="card">
|
||||
<view class="card-title">审批信息</view>
|
||||
|
||||
<view class="field">
|
||||
<text class="label">审批人<text class="req">*</text></text>
|
||||
<view class="input-like clickable" @tap="openEmployeePicker('assignee')">{{ employeeLoading ? '审批人加载中...' : (assigneeLabel || '请选择审批人') }}</view>
|
||||
</view>
|
||||
|
||||
<view class="field">
|
||||
<text class="label">抄送人</text>
|
||||
<view class="input-like clickable" @tap="openEmployeePicker('cc')">{{ employeeLoading ? '抄送人加载中...' : ccLabel }}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="card">
|
||||
<view class="card-title">说明</view>
|
||||
<view class="field">
|
||||
<text class="label">事由<text class="req">*</text></text>
|
||||
<textarea v-model="reason" class="textarea" placeholder="请说明出差目的与任务" />
|
||||
</view>
|
||||
<view class="field">
|
||||
<text class="label">附件</text>
|
||||
<view class="input-like clickable" @tap="openFilePicker">点击上传附件文件</view>
|
||||
<view v-if="uploadedFiles.length" class="file-list">
|
||||
<view v-for="(file, idx) in uploadedFiles" :key="file.ossId || file.url || idx" class="file-item">
|
||||
<text class="file-name">{{ file.originalName || file.fileName || file.name || file.url }}</text>
|
||||
<text class="remove" @tap.stop="removeFile(idx)">删除</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="field">
|
||||
<text class="label">备注</text>
|
||||
<textarea v-model="remark" class="textarea" placeholder="可选" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="card">
|
||||
<button class="btn primary" :loading="submitting" @tap="submit">提交申请</button>
|
||||
</view>
|
||||
|
||||
<x-file-picker ref="globalFilePicker" @select="onGlobalFileSelect" />
|
||||
<employee-picker
|
||||
:visible.sync="employeeSheetVisible"
|
||||
:title="employeeMode === 'assignee' ? '选择审批人' : '选择抄送人'"
|
||||
:list="employees"
|
||||
:multiple="employeeMode === 'cc'"
|
||||
:value="employeeMode === 'assignee' ? assignee : ccUsers"
|
||||
@change="onEmployeeChange"
|
||||
/>
|
||||
<date-time-picker :visible.sync="dateTimeSheetVisible" :value="dateValue" @change="onDateChange" />
|
||||
<city-picker :visible.sync="citySheetVisible" :value="destination" @change="onCityChange" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import RequestForm from '@/components/hrm/RequestForm.vue'
|
||||
import { addTravelReq } from '@/api/hrm/travel'
|
||||
import { listUser } from '@/api/oa/user'
|
||||
import { getEmployeeByUserId } from '@/api/hrm/employee'
|
||||
import { uploadFile } from '@/api/common/upload'
|
||||
import EmployeePicker from '@/components/hrm/EmployeePicker.vue'
|
||||
import DateTimePicker from '@/components/hrm/DateTimePicker.vue'
|
||||
import CityPicker from '@/components/hrm/CityPicker.vue'
|
||||
import XFilePicker from '@/components/x-native-uploader/x-file-picker.vue'
|
||||
|
||||
export default {
|
||||
components: { RequestForm },
|
||||
components: { EmployeePicker, DateTimePicker, CityPicker, XFilePicker },
|
||||
data() {
|
||||
return {
|
||||
initialForm: { travelType: '', startTime: '', endTime: '', destination: '', reason: '', applyFileIds: '', remark: '' },
|
||||
sections: [
|
||||
{ key: 'basic', title: '基础信息', fields: [
|
||||
{ key: 'travelType', label: '出差类型', type: 'select', required: true, placeholder: '请选择出差类型', options: ['客户拜访', '项目支持', '培训学习', '会议会展', '验收交付', '其他'] },
|
||||
{ key: 'startTime', label: '开始时间', type: 'datetime', required: true, placeholder: '请选择开始时间' },
|
||||
{ key: 'endTime', label: '结束时间', type: 'datetime', required: true, placeholder: '请选择结束时间' },
|
||||
{ key: 'destination', label: '目的地', type: 'city', required: true, placeholder: '请选择全球城市' }
|
||||
]},
|
||||
{ key: 'desc', title: '说明', fields: [
|
||||
{ key: 'reason', label: '事由', type: 'textarea', required: true, placeholder: '请说明出差目的与任务' },
|
||||
{ key: 'applyFileIds', label: '附件', type: 'file', required: false, placeholder: '上传附件文件' },
|
||||
{ key: 'remark', label: '备注', type: 'textarea', required: false, placeholder: '可选' }
|
||||
]}
|
||||
],
|
||||
flowFields: [
|
||||
{ key: 'travelType', label: '出差类型', required: true },
|
||||
{ key: 'startTime', label: '开始时间', required: true },
|
||||
{ key: 'endTime', label: '结束时间', required: true },
|
||||
{ key: 'destination', label: '目的地', required: true },
|
||||
{ key: 'reason', label: '事由', required: true }
|
||||
]
|
||||
travelTypes: ['客户拜访', '项目支持', '培训学习', '会议会展', '验收交付', '其他'],
|
||||
travelType: '',
|
||||
startTime: '',
|
||||
endTime: '',
|
||||
destination: '',
|
||||
estimatedCost: '',
|
||||
reason: '',
|
||||
remark: '',
|
||||
accessoryApplyIds: '',
|
||||
uploadedFiles: [],
|
||||
submitting: false,
|
||||
employees: [],
|
||||
employeeLoading: true,
|
||||
employeeSheetVisible: false,
|
||||
employeeMode: 'assignee',
|
||||
assignee: null,
|
||||
ccUsers: [],
|
||||
dateTimeSheetVisible: false,
|
||||
dateFieldKey: '',
|
||||
dateValue: '',
|
||||
citySheetVisible: false,
|
||||
cachedAssigneeKey: '',
|
||||
currentEmpId: null
|
||||
}
|
||||
},
|
||||
methods: { submitTravel(payload) { return addTravelReq(payload) } }
|
||||
computed: {
|
||||
assigneeLabel() {
|
||||
return this.assignee ? this.employeeName(this.assignee) : ''
|
||||
},
|
||||
ccLabel() {
|
||||
return this.ccUsers.length ? '已选择 ' + this.ccUsers.length + ' 人' : '点击选择抄送人'
|
||||
}
|
||||
},
|
||||
async created() {
|
||||
this.cachedAssigneeKey = 'hrm_manual_assignee_' + 'travel'
|
||||
await this.loadEmployees()
|
||||
await this.loadCurrentEmpId()
|
||||
this.restoreAssignee()
|
||||
},
|
||||
methods: {
|
||||
employeeName(emp) {
|
||||
return emp && (emp.empName || emp.nickName || emp.userName || emp.realName) || ('员工' + ((emp && (emp.userId || emp.id)) || ''))
|
||||
},
|
||||
employeeDept(emp) {
|
||||
return (emp && emp.dept && emp.dept.deptName) || (emp && emp.deptName) || '未分配部门'
|
||||
},
|
||||
async loadEmployees() {
|
||||
this.employeeLoading = true
|
||||
try {
|
||||
var res = await listUser({ pageNum: 1, pageSize: 500 })
|
||||
this.employees = res.rows || res.data || []
|
||||
} catch (e) {
|
||||
this.employees = []
|
||||
} finally {
|
||||
this.employeeLoading = false
|
||||
}
|
||||
},
|
||||
async loadCurrentEmpId() {
|
||||
try {
|
||||
var oaId = uni.getStorageSync('oaId')
|
||||
if (!oaId) return
|
||||
var emp = await getEmployeeByUserId(oaId)
|
||||
if (emp && emp.data && emp.data.empId) {
|
||||
this.currentEmpId = emp.data.empId
|
||||
}
|
||||
} catch (e) {}
|
||||
},
|
||||
restoreAssignee() {
|
||||
try {
|
||||
var raw = uni.getStorageSync(this.cachedAssigneeKey)
|
||||
if (!raw) return
|
||||
var cached = typeof raw === 'string' ? JSON.parse(raw) : raw
|
||||
var id = String(cached.userId || cached.empId || '')
|
||||
var hit = this.employees.find(function (e) { return String(e.userId || e.id) === id })
|
||||
if (hit) this.assignee = hit
|
||||
} catch (e) {}
|
||||
},
|
||||
openEmployeePicker(mode) {
|
||||
if (this.employeeLoading) {
|
||||
uni.showToast({ title: '人员加载中,请稍候', icon: 'none' })
|
||||
return
|
||||
}
|
||||
this.employeeMode = mode
|
||||
this.employeeSheetVisible = true
|
||||
},
|
||||
onEmployeeChange(val) {
|
||||
if (this.employeeMode === 'assignee') {
|
||||
this.assignee = val
|
||||
try {
|
||||
uni.setStorageSync(this.cachedAssigneeKey, JSON.stringify({
|
||||
userId: val && (val.userId || val.id),
|
||||
empId: val && val.empId || '',
|
||||
empName: this.employeeName(val),
|
||||
deptName: this.employeeDept(val)
|
||||
}))
|
||||
} catch (e) {}
|
||||
} else {
|
||||
this.ccUsers = val || []
|
||||
}
|
||||
},
|
||||
onTravelTypeChange(e) {
|
||||
var idx = Number(e.detail.value)
|
||||
this.travelType = this.travelTypes[idx] || ''
|
||||
},
|
||||
openDateTime(fieldKey) {
|
||||
this.dateFieldKey = fieldKey
|
||||
this.dateValue = this[fieldKey] || ''
|
||||
this.dateTimeSheetVisible = true
|
||||
},
|
||||
onDateChange(val) {
|
||||
if (!this.dateFieldKey) return
|
||||
this[this.dateFieldKey] = val
|
||||
},
|
||||
formatDateTime(v) {
|
||||
if (!v) return ''
|
||||
return String(v).replace('T', ' ').substring(0, 16)
|
||||
},
|
||||
openCityPicker() {
|
||||
this.citySheetVisible = true
|
||||
},
|
||||
onCityChange(val) {
|
||||
this.destination = val || ''
|
||||
},
|
||||
onEstimatedCostInput(e) {
|
||||
this.estimatedCost = e.detail.value
|
||||
},
|
||||
openFilePicker() {
|
||||
var picker = this.$refs.globalFilePicker
|
||||
if (picker && picker.open) picker.open()
|
||||
},
|
||||
async onGlobalFileSelect(payload) {
|
||||
var files = Array.isArray(payload) ? payload : [payload]
|
||||
for (var i = 0; i < files.length; i++) {
|
||||
var f = files[i]
|
||||
if (!f) continue
|
||||
try {
|
||||
var uploaded = await uploadFile({ path: f.path || f.url || '', name: f.name || f.fileName || 'file', size: f.size || 0, type: f.type || '' })
|
||||
this.uploadedFiles.push({
|
||||
name: uploaded.fileName || f.name || f.fileName || 'file',
|
||||
fileName: uploaded.fileName || f.fileName || f.name || '',
|
||||
originalName: f.name || f.fileName || uploaded.fileName || '',
|
||||
url: uploaded.url || f.path || f.url || '',
|
||||
ossId: uploaded.ossId || uploaded.url || f.path || f.url || ''
|
||||
})
|
||||
} catch (e) {
|
||||
uni.showToast({ title: e.message || '上传失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
this.syncFileIds()
|
||||
},
|
||||
removeFile(idx) {
|
||||
this.uploadedFiles.splice(idx, 1)
|
||||
this.syncFileIds()
|
||||
},
|
||||
syncFileIds() {
|
||||
this.accessoryApplyIds = this.uploadedFiles.map(function (f) { return f.ossId }).join(',')
|
||||
},
|
||||
validate() {
|
||||
if (!this.assignee) return uni.showToast({ title: '请选择审批人', icon: 'none' }), false
|
||||
if (!this.travelType) return uni.showToast({ title: '请选择出差类型', icon: 'none' }), false
|
||||
if (!this.startTime) return uni.showToast({ title: '请选择开始时间', icon: 'none' }), false
|
||||
if (!this.endTime) return uni.showToast({ title: '请选择结束时间', icon: 'none' }), false
|
||||
if (!this.destination) return uni.showToast({ title: '请选择目的地', icon: 'none' }), false
|
||||
if (!this.reason) return uni.showToast({ title: '请填写事由', icon: 'none' }), false
|
||||
return true
|
||||
},
|
||||
async submit() {
|
||||
if (!this.validate()) return
|
||||
this.submitting = true
|
||||
try {
|
||||
await addTravelReq({
|
||||
empId: this.currentEmpId,
|
||||
travelType: this.travelType,
|
||||
startTime: this.startTime,
|
||||
endTime: this.endTime,
|
||||
destination: this.destination,
|
||||
estimatedCost: this.estimatedCost,
|
||||
reason: this.reason,
|
||||
accessoryApplyIds: this.accessoryApplyIds,
|
||||
remark: this.remark,
|
||||
status: 'pending',
|
||||
bizType: 'travel',
|
||||
manualAssigneeUserId: this.assignee && (this.assignee.userId || this.assignee.id),
|
||||
ccUserIds: this.ccUsers.map(function (u) { return u.userId || u.id }).join(',')
|
||||
})
|
||||
uni.showToast({ title: '提交成功', icon: 'success' })
|
||||
setTimeout(() => uni.navigateBack({ delta: 1 }), 500)
|
||||
} catch (e) {
|
||||
uni.showToast({ title: e.message || '提交失败', icon: 'none' })
|
||||
} finally {
|
||||
this.submitting = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
<style scoped>
|
||||
.page{min-height:100vh;background:#f5f7fb;padding:24rpx;box-sizing:border-box}.card{background:#fff;border-radius:20rpx;padding:24rpx;margin-bottom:20rpx}.card-title{font-size:30rpx;font-weight:700;color:#111827;margin-bottom:16rpx}.field{margin-bottom:18rpx}.label{display:block;margin-bottom:8rpx;font-size:24rpx;color:#6b7280}.req{color:#ef4444;margin-left:4rpx}.input-like,.input,.textarea{width:100%;box-sizing:border-box;border:1rpx solid #e5e7eb;border-radius:16rpx;background:#f9fafb;padding:20rpx;font-size:28rpx;color:#111827}.input{min-height:88rpx}.clickable{color:#111827}.textarea{min-height:160rpx}.file-list{margin-top:16rpx;display:flex;flex-direction:column;gap:12rpx}.file-item{display:flex;align-items:center;justify-content:space-between;padding:18rpx 18rpx;border-radius:16rpx;background:#f8fafc;border:1rpx solid #e5eefc}.file-name{flex:1;font-size:26rpx;color:#334155;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;margin-right:16rpx}.remove{font-size:24rpx;color:#ef4444;flex-shrink:0}.btn.primary{width:100%;background:#1677ff;color:#fff;border-radius:16rpx}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user